commit 8dd4dd32efbb99a6e47a511c8db0cda17365d233 Author: Enki Date: Thu Nov 21 01:05:37 2024 -0800 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d51920 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Bluesky Connector Plugin for WordPress + +## Description +This plugin connects your WordPress blog to Bluesky and allows you to format and publish posts to Bluesky whenever a new post is published. Users can log into their Bluesky account via the plugin, and the plugin will automatically generate and store the API key. + +## Installation +1. Upload the `bluesky-connector` folder to the `/wp-content/plugins/` directory. +2. Activate the plugin through the 'Plugins' menu in WordPress. +3. Configure the plugin settings in the WordPress admin under 'Settings' > 'Bluesky Connector'. + +## Configuration +- **Client ID**: Your Bluesky client ID. +- **Client Secret**: Your Bluesky client secret. +- **Redirect URI**: Your Bluesky redirect URI. + +## Usage +- Once configured, click on the "Login with Bluesky" button to authenticate and generate the API key. +- The plugin will automatically publish a formatted version of your new posts to Bluesky. + +## License +This plugin is licensed under the [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html). \ No newline at end of file diff --git a/admin/settings.php b/admin/settings.php new file mode 100644 index 0000000..1b9d02c --- /dev/null +++ b/admin/settings.php @@ -0,0 +1,2 @@ + 'string', + 'default' => 'https://bsky.social', + 'sanitize_callback' => function($input) { + // Force the default if empty + if (empty($input)) { + return 'https://bsky.social'; + } + // Ensure https:// is present + if (strpos($input, 'https://') !== 0) { + $input = 'https://' . $input; + } + // Remove any trailing slashes + return rtrim($input, '/'); + } + )); + register_setting('bluesky_connector_settings', 'bluesky_identifier', array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field' + )); + + register_setting('bluesky_connector_settings', 'bluesky_password', array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field' + )); + + register_setting('bluesky_connector_settings', 'bluesky_post_format', array( + 'type' => 'string', + 'default' => 'title-excerpt-image-link', + 'sanitize_callback' => 'sanitize_text_field' + )); + register_setting('bluesky_connector_settings', 'bluesky_include_title', array( + 'type' => 'boolean', + 'default' => true + )); + register_setting('bluesky_connector_settings', 'bluesky_title_separator', array( + 'type' => 'string', + 'default' => "\n\n", + 'sanitize_callback' => 'sanitize_text_field' + )); + + // Add settings sections and fields + add_settings_section( + 'bluesky_connector_main', + __('Bluesky Connection Settings', 'bluesky-connector'), + array($this, 'render_settings_section'), + 'bluesky_connector_settings' + ); + + add_settings_section( + 'bluesky_format_section', + __('Post Format Settings', 'bluesky-connector'), + array($this, 'render_format_section'), + 'bluesky_connector_settings' + ); + + add_settings_field( + 'bluesky_domain', + __('Bluesky Domain', 'bluesky-connector'), + array($this, 'render_domain_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_identifier', + __('Bluesky Handle', 'bluesky-connector'), + array($this, 'render_identifier_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_password', + __('App Password', 'bluesky-connector'), + array($this, 'render_password_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_post_format', + __('Post Layout', 'bluesky-connector'), + array($this, 'render_post_format_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + + add_settings_field( + 'bluesky_include_title', + __('Include Post Title', 'bluesky-connector'), + array($this, 'render_include_title_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + + add_settings_field( + 'bluesky_title_separator', + __('Title Separator', 'bluesky-connector'), + array($this, 'render_title_separator_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + } + + + + + /** + * Add admin menu + */ + public function add_admin_menu() + { + add_options_page( + __('Bluesky Connector Settings', 'bluesky-connector'), + __('Bluesky Connector', 'bluesky-connector'), + 'manage_options', + 'bluesky-connector', + array($this, 'render_settings_page') + ); + } + + /** + * Render settings page + */ + public function render_settings_page() + { + if (!current_user_can('manage_options')) { + return; + } + + // Add process queue button + if (isset($_POST['process_queue_now']) && check_admin_referer('bluesky_process_queue')) { + $this->process_post_queue(); + add_settings_error( + 'bluesky_connector_settings', + 'queue_processed', + __('Queue processed.', 'bluesky-connector'), + 'success' + ); + } + + // Check for form submission + if (isset($_POST['action']) && $_POST['action'] === 'update_bluesky_settings') { + check_admin_referer('bluesky_connector_settings'); + $this->handle_settings_update(); + } + + // Get current settings + $settings = array( + 'domain' => get_option('bluesky_domain', 'https://bsky.social'), + 'identifier' => get_option('bluesky_identifier', ''), + 'connection_status' => get_option('bluesky_connection_status', ''), + 'last_error' => get_option('bluesky_last_error', '') + ); + + include BLUESKY_CONNECTOR_DIR . 'templates/settings-page.php'; + } + + /** + * Render settings section description + */ + public function render_settings_section() + { + echo '

' . esc_html__('Configure your Bluesky connection settings below.', 'bluesky-connector') . '

'; + } + + /** + * Render domain field + */ + public function render_domain_field() + { + $value = get_option('bluesky_domain', 'https://bsky.social'); + echo ''; + echo '

' . esc_html__('The Bluesky API domain (default: https://bsky.social)', 'bluesky-connector') . '

'; + } + + /** + * Render identifier field + */ + public function render_identifier_field() + { + $value = get_option('bluesky_identifier', ''); + echo ''; + echo '

' . esc_html__('Your Bluesky handle (e.g., username.bsky.social)', 'bluesky-connector') . '

'; + } + + /** + * Render password field + */ + public function render_password_field() + { + echo ''; + echo '

' . esc_html__('Your Bluesky app password (will not be stored)', 'bluesky-connector') . '

'; + } + + /** + * Handle settings update + */ + private function handle_settings_update() + { + $identifier = sanitize_text_field($_POST['bluesky_identifier']); + $password = sanitize_text_field($_POST['bluesky_password']); + $domain = esc_url_raw($_POST['bluesky_domain']); + + if (empty($identifier) || empty($password)) { + add_settings_error( + 'bluesky_connector_settings', + 'missing_credentials', + __('Both identifier and password are required.', 'bluesky-connector') + ); + return; + } + + // Try to authenticate + $auth = new Bluesky_Auth($identifier, $password); + $result = $auth->get_access_token(); + + if (isset($result['error'])) { + update_option('bluesky_connection_status', 'error'); + update_option('bluesky_last_error', $result['error']); + add_settings_error( + 'bluesky_connector_settings', + 'auth_failed', + sprintf(__('Authentication failed: %s', 'bluesky-connector'), $result['error']) + ); + } else { + update_option('bluesky_connection_status', 'connected'); + update_option('bluesky_last_error', ''); + add_settings_error( + 'bluesky_connector_settings', + 'settings_updated', + __('Settings saved and connected successfully.', 'bluesky-connector'), + 'success' + ); + } + } + + /** + * Handle post publish + */ + public function handle_post_publish($post_id, $post) + { + // Skip if not a public post + if ($post->post_status !== 'publish' || $post->post_type !== 'post') { + return; + } + + // Skip if already posted + if (get_post_meta($post_id, '_bluesky_posted', true)) { + return; + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + } + + /** + * Process post queue + */ + public function process_post_queue() + { + $queue = get_option('bluesky_post_queue', array()); + if (empty($queue)) { + return; + } + + $access_token = get_option('bluesky_access_jwt'); + $did = get_option('bluesky_did'); + + if (!$access_token || !$did) { + error_log('Bluesky Connector: Missing access token or DID'); + return; + } + + foreach ($queue as $key => $post_id) { + $post = get_post($post_id); + if (!$post) { + unset($queue[$key]); + continue; + } + + try { + $formatter = new Post_Formatter($access_token, $did); + $response = $formatter->format_and_post($post); + + if (!isset($response['error'])) { + unset($queue[$key]); + update_post_meta($post_id, '_bluesky_post_id', $response['uri']); + update_post_meta($post_id, '_bluesky_posted', current_time('mysql')); + update_post_meta($post_id, '_bluesky_status', 'success'); + } else { + update_post_meta($post_id, '_bluesky_status', 'error'); + update_post_meta($post_id, '_bluesky_error', $response['error']); + error_log('Bluesky Post Error: ' . print_r($response['error'], true)); + } + } catch (Exception $e) { + error_log('Bluesky Connector Exception: ' . $e->getMessage()); + update_post_meta($post_id, '_bluesky_status', 'error'); + update_post_meta($post_id, '_bluesky_error', $e->getMessage()); + } + } + + update_option('bluesky_post_queue', array_values($queue)); + } + + /** + * Add post meta box + */ + public function add_post_meta_box() + { + add_meta_box( + 'bluesky_post_status', + __('Bluesky Status', 'bluesky-connector'), + array($this, 'render_post_meta_box'), + 'post', + 'side', + 'default' + ); + } + + /** + * Render post meta box + */ + public function render_post_meta_box($post) + { + $status = get_post_meta($post->ID, '_bluesky_status', true); + $posted_date = get_post_meta($post->ID, '_bluesky_posted', true); + $error = get_post_meta($post->ID, '_bluesky_error', true); + $post_id = get_post_meta($post->ID, '_bluesky_post_id', true); + + include BLUESKY_CONNECTOR_DIR . 'templates/post-meta-box.php'; + } + + /** + * Save post meta box + */ + public function save_post_meta_box($post_id) + { + if (!current_user_can('edit_post', $post_id)) { + return; + } + + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + // Add any custom meta box saving logic here + } + + /** + * Handle retry post AJAX request + */ + public function handle_retry_post() + { + check_ajax_referer('bluesky_admin', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(['message' => __('Permission denied.', 'bluesky-connector')]); + } + + $post_id = intval($_POST['post_id']); + $post = get_post($post_id); + + if (!$post) { + wp_send_json_error(['message' => __('Post not found.', 'bluesky-connector')]); + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + + // Update post meta + update_post_meta($post_id, '_bluesky_status', 'queued'); + delete_post_meta($post_id, '_bluesky_error'); + + wp_send_json_success(['message' => __('Post queued for retry.', 'bluesky-connector')]); + } + + /** + * Handle share post AJAX request + */ + public function handle_share_post() + { + check_ajax_referer('bluesky_admin', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(['message' => __('Permission denied.', 'bluesky-connector')]); + } + + $post_id = intval($_POST['post_id']); + $post = get_post($post_id); + + if (!$post) { + wp_send_json_error(['message' => __('Post not found.', 'bluesky-connector')]); + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + + // Update post meta + update_post_meta($post_id, '_bluesky_status', 'queued'); + + wp_send_json_success(['message' => __('Post queued for sharing.', 'bluesky-connector')]); + } + + /** + * Enqueue admin scripts and styles + */ + public function enqueue_admin_scripts($hook) + { + if ('post.php' !== $hook && 'post-new.php' !== $hook) { + return; + } + + wp_enqueue_script( + 'bluesky-admin', + BLUESKY_CONNECTOR_URL . 'assets/js/admin.js', + array('jquery'), + BLUESKY_CONNECTOR_VERSION, + true + ); + + wp_localize_script('bluesky-admin', 'blueskyAdmin', array( + 'nonce' => wp_create_nonce('bluesky_admin'), + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'strings' => array( + 'error' => __('An error occurred. Please try again.', 'bluesky-connector'), + 'success' => __('Operation completed successfully.', 'bluesky-connector') + ) + )); + + wp_enqueue_style( + 'bluesky-admin', + BLUESKY_CONNECTOR_URL . 'assets/css/admin.css', + array(), + BLUESKY_CONNECTOR_VERSION + ); + } + + /** + * Plugin activation + */ + public function activate() + { + // Create necessary options with default values + add_option('bluesky_domain', 'https://bsky.social'); + add_option('bluesky_post_queue', array()); + add_option('bluesky_connection_status', ''); + + // Schedule cron job + if (!wp_next_scheduled('process_bluesky_post_queue')) { + wp_schedule_event(time(), 'hourly', 'process_bluesky_post_queue'); + } + + // Create custom capabilities + $role = get_role('administrator'); + if ($role) { + $role->add_cap('manage_bluesky_settings'); + } + + // Flush rewrite rules + flush_rewrite_rules(); + } + + /** + * Plugin deactivation + */ + public function deactivate() + { + // Clear scheduled events + wp_clear_scheduled_hook('process_bluesky_post_queue'); + + // Remove custom capabilities + $role = get_role('administrator'); + if ($role) { + $role->remove_cap('manage_bluesky_settings'); + } + } + + /** + * Display admin notices + */ + public function admin_notices() + { + if (!current_user_can('manage_options')) { + return; + } + + $screen = get_current_screen(); + if ($screen->id !== 'settings_page_bluesky-connector') { + return; + } + + settings_errors('bluesky_connector_settings'); + } + + /** + * Add settings link to plugins page + */ + public function add_settings_link($links) + { + $settings_link = sprintf( + '%s', + admin_url('options-general.php?page=bluesky-connector'), + __('Settings', 'bluesky-connector') + ); + array_unshift($links, $settings_link); + return $links; + } + + /** + * Log plugin errors + */ + private function log_error($message, $data = array()) + { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log(sprintf( + '[Bluesky Connector] %s | Data: %s', + $message, + print_r($data, true) + )); + } + } + + /** + * Filter for customizing post content before sending to Bluesky + */ + public function filter_post_content($content, $post) + { + return apply_filters('bluesky_post_content', $content, $post); + } + + /** + * Check if post should be shared to Bluesky + */ + private function should_share_post($post) + { + // Skip if post is not published + if ($post->post_status !== 'publish') { + return false; + } + + // Skip if post type is not supported + $supported_post_types = apply_filters('bluesky_supported_post_types', array('post')); + if (!in_array($post->post_type, $supported_post_types)) { + return false; + } + + // Skip if already shared + if (get_post_meta($post->ID, '_bluesky_posted', true)) { + return false; + } + + // Allow custom filtering + return apply_filters('bluesky_should_share_post', true, $post); + } + + public function render_format_section() { + echo '

' . esc_html__('Customize how your posts appear on Bluesky.', 'bluesky-connector') . '

'; + } + + public function render_post_format_field() { + $format = get_option('bluesky_post_format', 'title-excerpt-image-link'); + $options = array( + 'title-excerpt-image-link' => __('Title → Excerpt → Image → Link', 'bluesky-connector'), + 'title-image-excerpt-link' => __('Title → Image → Excerpt → Link', 'bluesky-connector'), + 'image-title-excerpt-link' => __('Image → Title → Excerpt → Link', 'bluesky-connector'), + 'excerpt-image-link' => __('Excerpt → Image → Link', 'bluesky-connector'), + 'title-excerpt-link' => __('Title → Excerpt → Link (No Image)', 'bluesky-connector'), + ); + + echo ''; + echo '

' . esc_html__('Choose how you want your posts to be formatted on Bluesky.', 'bluesky-connector') . '

'; + } + + public function render_include_title_field() { + $include_title = get_option('bluesky_include_title', true); + printf( + '', + checked($include_title, true, false), + __('Include post title at the beginning', 'bluesky-connector') + ); + echo '

' . esc_html__('Add the post title to the beginning of each Bluesky post.', 'bluesky-connector') . '

'; + } + + public function render_title_separator_field() { + $separator = get_option('bluesky_title_separator', "\n\n"); + $options = array( + "\n\n" => __('Double Line Break', 'bluesky-connector'), + "\n" => __('Single Line Break', 'bluesky-connector'), + " - " => __('Dash', 'bluesky-connector'), + ": " => __('Colon', 'bluesky-connector'), + ); + + echo ''; + echo '

' . esc_html__('Choose how to separate the title from the excerpt.', 'bluesky-connector') . '

'; + } +} + +/** + * Initialize the plugin + */ +function bluesky_connector_init() +{ + return Bluesky_Connector::get_instance(); +} + +// Initialize the plugin +add_action('plugins_loaded', 'bluesky_connector_init'); + +/** + * Register uninstall hook + */ +register_uninstall_hook(__FILE__, 'bluesky_connector_uninstall'); + +/** + * Clean up plugin data on uninstall + */ +function bluesky_connector_uninstall() +{ + // Only run if explicitly uninstalling + if (!defined('WP_UNINSTALL_PLUGIN')) { + return; + } + + // Remove options + delete_option('bluesky_domain'); + delete_option('bluesky_identifier'); + delete_option('bluesky_access_jwt'); + delete_option('bluesky_refresh_jwt'); + delete_option('bluesky_did'); + delete_option('bluesky_post_queue'); + delete_option('bluesky_connection_status'); + delete_option('bluesky_last_error'); + + // Remove capabilities + $role = get_role('administrator'); + if ($role) { + $role->remove_cap('manage_bluesky_settings'); + } + + // Clear scheduled hooks + wp_clear_scheduled_hook('process_bluesky_post_queue'); + + // Remove post meta + global $wpdb; + $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_bluesky_%'"); +} \ No newline at end of file diff --git a/includes/bluesky-api.php b/includes/bluesky-api.php new file mode 100644 index 0000000..213e096 --- /dev/null +++ b/includes/bluesky-api.php @@ -0,0 +1,70 @@ +api_key = $api_key; + $this->did = $did; + } + + public function create_post($post_data) { + $headers = array( + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => 'application/json', + ); + + $response = wp_remote_post($this->api_url . '/com.atproto.repo.createRecord', array( + 'headers' => $headers, + 'body' => json_encode(array( + 'repo' => $this->did, + 'collection' => 'app.bsky.feed.post', + 'record' => $post_data, + )), + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } + + public function resolve_handle($handle) { + $response = wp_remote_get($this->api_url . '/com.atproto.identity.resolveHandle', array( + 'query' => array( + 'handle' => $handle, + ), + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } + + public function upload_blob($file_path, $mime_type) { + $headers = array( + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => $mime_type, + ); + + $file_contents = file_get_contents($file_path); + if ($file_contents === false) { + return array('error' => 'Failed to read file.'); + } + + $response = wp_remote_post($this->api_url . '/com.atproto.repo.uploadBlob', array( + 'headers' => $headers, + 'body' => $file_contents, + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } +} \ No newline at end of file diff --git a/includes/bluesky-auth.php b/includes/bluesky-auth.php new file mode 100644 index 0000000..11e0d16 --- /dev/null +++ b/includes/bluesky-auth.php @@ -0,0 +1,152 @@ +identifier = $identifier; + $this->password = $password; + + // Get domain with fallback and force https:// + $domain = get_option('bluesky_domain', 'https://bsky.social'); + if (empty($domain)) { + $domain = 'https://bsky.social'; + } + if (strpos($domain, 'https://') !== 0) { + $domain = 'https://' . $domain; + } + + // Ensure proper URL format + $this->api_domain = rtrim($domain, '/') . '/'; + + // Debug logs + error_log('Bluesky Auth - Using domain setting: ' . get_option('bluesky_domain')); + error_log('Bluesky Auth - Processed domain: ' . $this->api_domain); + error_log('Bluesky Auth - Identifier: ' . $this->identifier); + } + + public function get_access_token() { + if ($this->should_refresh_token()) { + return $this->refresh_access_token(); + } + + $token_url = $this->api_domain . 'xrpc/com.atproto.server.createSession'; + + // Debug log + error_log('Bluesky Auth - Attempting connection to: ' . $token_url); + + $headers = array( + 'Content-Type' => 'application/json', + ); + + $body = json_encode(array( + 'identifier' => $this->identifier, + 'password' => $this->password, + )); + + // Debug log + error_log('Bluesky Auth - Request body: ' . $body); + + $wp_version = get_bloginfo('version'); + $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); + + $response = wp_remote_post($token_url, array( + 'headers' => $headers, + 'user-agent' => "$user_agent; Bluesky Connector", + 'body' => $body, + 'timeout' => 30, // Increase timeout + )); + + if (is_wp_error($response)) { + $error_message = $response->get_error_message(); + error_log('Bluesky Auth Error: ' . $error_message); + return array('error' => $error_message); + } + + // Debug response + $status_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('Bluesky Auth - Response status: ' . $status_code); + error_log('Bluesky Auth - Response body: ' . $response_body); + + $data = json_decode($response_body, true); + + if (!empty($data['accessJwt']) && !empty($data['refreshJwt']) && !empty($data['did'])) { + update_option('bluesky_access_jwt', sanitize_text_field($data['accessJwt'])); + update_option('bluesky_refresh_jwt', sanitize_text_field($data['refreshJwt'])); + update_option('bluesky_did', sanitize_text_field($data['did'])); + update_option('bluesky_token_created', time()); + delete_option('bluesky_password'); // Don't store password + return $data['accessJwt']; + } + + // More detailed error reporting + $error_message = isset($data['error']) ? $data['error'] : 'Failed to get access token'; + if (isset($data['message'])) { + $error_message .= ' - ' . $data['message']; + } + error_log('Bluesky Auth - Error: ' . $error_message); + return array('error' => $error_message); + } + + private function should_refresh_token() { + $token_created = get_option('bluesky_token_created'); + $refresh_token = get_option('bluesky_refresh_jwt'); + + // Refresh if token is older than 23 hours or doesn't exist + return !empty($refresh_token) && ($token_created < (time() - 82800)); + } + + private function refresh_access_token() { + $refresh_token = get_option('bluesky_refresh_jwt'); + if (empty($refresh_token)) { + return array('error' => 'No refresh token available'); + } + + $refresh_url = $this->api_domain . 'xrpc/com.atproto.server.refreshSession'; + + // Debug log + error_log('Bluesky Auth - Attempting token refresh at: ' . $refresh_url); + + $wp_version = get_bloginfo('version'); + $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); + + $response = wp_remote_post($refresh_url, array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $refresh_token, + 'Content-Type' => 'application/json', + ), + 'user-agent' => "$user_agent; Bluesky Connector", + 'timeout' => 30, + )); + + if (is_wp_error($response)) { + $error_message = $response->get_error_message(); + error_log('Bluesky Token Refresh Error: ' . $error_message); + return array('error' => $error_message); + } + + // Debug response + $status_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('Bluesky Auth Refresh - Response status: ' . $status_code); + error_log('Bluesky Auth Refresh - Response body: ' . $response_body); + + $data = json_decode($response_body, true); + + if (!empty($data['accessJwt']) && !empty($data['refreshJwt'])) { + update_option('bluesky_access_jwt', sanitize_text_field($data['accessJwt'])); + update_option('bluesky_refresh_jwt', sanitize_text_field($data['refreshJwt'])); + update_option('bluesky_token_created', time()); + return $data['accessJwt']; + } + + $error_message = isset($data['error']) ? $data['error'] : 'Failed to refresh token'; + if (isset($data['message'])) { + $error_message .= ' - ' . $data['message']; + } + error_log('Bluesky Auth Refresh - Error: ' . $error_message); + return array('error' => $error_message); + } +} \ No newline at end of file diff --git a/includes/post-formatter.php b/includes/post-formatter.php new file mode 100644 index 0000000..5ba409a --- /dev/null +++ b/includes/post-formatter.php @@ -0,0 +1,243 @@ +api = new Bluesky_API($access_token, $did); + } + + public function format_and_post($post) { + $content = $this->get_formatted_content($post); + + $post_data = array( + '$type' => 'app.bsky.feed.post', + 'text' => $content, + 'createdAt' => gmdate('c', strtotime($post->post_date_gmt)), + 'langs' => array('en') + ); + + // Add facets for the URL + $post_data['facets'] = $this->parse_facets($post); + + // Handle image embed separately from text content + if (has_post_thumbnail($post->ID)) { + $image_data = $this->handle_featured_image($post->ID); + if (!empty($image_data) && !isset($image_data['error'])) { + $post_data['embed'] = $image_data; + } + } + + return $this->api->create_post($post_data); + } + + private function get_formatted_content($post) { + $format = get_option('bluesky_post_format', 'image-title-excerpt-link'); + $include_title = get_option('bluesky_include_title', true); + + // Get individual components + $title = $include_title ? $post->post_title : ''; + $excerpt = $this->get_excerpt($post); + $url = wp_get_shortlink($post->ID); + + // Start building content with explicit line breaks + $content = ''; + + // Add title with line break if it exists + if (!empty($title)) { + $content .= $title . "\n\n"; + } + + // Add excerpt if it exists + if (!empty($excerpt)) { + $content .= $excerpt . "\n\n"; // Add double line break after excerpt + } + + // Add URL on its own line + if (!empty($url)) { + $content .= $url; // URL starts on new line due to previous \n\n + } + + return $content; + } + + private function get_excerpt($post) { + $text = wp_strip_all_tags($post->post_excerpt); + if (empty($text)) { + $text = wp_strip_all_tags($post->post_content); + } + + $url = wp_get_shortlink($post->ID); + $include_title = get_option('bluesky_include_title', true); + + // Calculate available length accounting for spacing and new lines + $available_length = $this->max_length; + $available_length -= strlen($url); + $available_length -= 2; // Account for \n\n after excerpt + + if ($include_title) { + $available_length -= strlen($post->post_title); + $available_length -= 2; // Account for \n\n after title + } + + if (mb_strlen($text) > $available_length) { + $text = mb_substr($text, 0, $available_length - 3) . '...'; + } + + return $text; + } + + private function parse_facets($post) { + $facets = array(); + $content = $this->get_formatted_content($post); + + // Add link facet for the post URL + $url = wp_get_shortlink($post->ID); + $text_bytes = mb_convert_encoding($content, 'UTF-8'); + $url_position = mb_strrpos($text_bytes, $url); + + if ($url_position !== false) { + $facets[] = array( + 'index' => array( + 'byteStart' => $url_position, + 'byteEnd' => $url_position + strlen($url), + ), + 'features' => array( + array( + '$type' => 'app.bsky.richtext.facet#link', + 'uri' => $url, + ), + ), + ); + } + + return $facets; + } + + private function handle_featured_image($post_id) { + $image_id = get_post_thumbnail_id($post_id); + $image_path = get_attached_file($image_id); + + if (!$image_path) { + return array('error' => 'Image file not found'); + } + + $mime_type = get_post_mime_type($image_id); + + // Get image data + $image_data = file_get_contents($image_path); + if ($image_data === false) { + return array('error' => 'Failed to read image file'); + } + + // Check file size (1MB limit for Bluesky) + if (strlen($image_data) > 1000000) { + // If image is too large, attempt to resize it + $resized = $this->resize_image($image_path); + if ($resized) { + $image_data = file_get_contents($resized); + unlink($resized); // Clean up temporary file + } else { + return array('error' => 'Image file size exceeds 1MB limit and resize failed'); + } + } + + // Upload image blob + $response = $this->api->upload_blob($image_path, $mime_type); + + if (isset($response['error'])) { + return $response; + } + + // Get the alt text + $alt_text = get_post_meta($image_id, '_wp_attachment_image_alt', true) ?: ''; + + return array( + '$type' => 'app.bsky.embed.images', + 'images' => array( + array( + 'alt' => $alt_text, + 'image' => $response['blob'], + ), + ), + ); + } + + private function resize_image($image_path) { + // Only proceed if GD is available + if (!function_exists('imagecreatefrompng')) { + return false; + } + + $mime_type = mime_content_type($image_path); + list($width, $height) = getimagesize($image_path); + + // Calculate new dimensions while maintaining aspect ratio + $max_dimension = 1000; // Reasonable size that should result in < 1MB file + if ($width > $height) { + $new_width = $max_dimension; + $new_height = floor($height * ($max_dimension / $width)); + } else { + $new_height = $max_dimension; + $new_width = floor($width * ($max_dimension / $height)); + } + + // Create new image + $new_image = imagecreatetruecolor($new_width, $new_height); + + // Handle different image types + switch ($mime_type) { + case 'image/jpeg': + $source = imagecreatefromjpeg($image_path); + break; + case 'image/png': + $source = imagecreatefrompng($image_path); + // Preserve transparency + imagealphablending($new_image, false); + imagesavealpha($new_image, true); + break; + case 'image/gif': + $source = imagecreatefromgif($image_path); + break; + default: + return false; + } + + if (!$source) { + return false; + } + + // Resize + imagecopyresampled( + $new_image, + $source, + 0, 0, 0, 0, + $new_width, + $new_height, + $width, + $height + ); + + // Create temporary file + $temp_file = tempnam(sys_get_temp_dir(), 'bluesky_img_'); + + // Save resized image + switch ($mime_type) { + case 'image/jpeg': + imagejpeg($new_image, $temp_file, 85); + break; + case 'image/png': + imagepng($new_image, $temp_file, 8); + break; + case 'image/gif': + imagegif($new_image, $temp_file); + break; + } + + // Clean up + imagedestroy($source); + imagedestroy($new_image); + + return $temp_file; + } +} \ No newline at end of file diff --git a/includes/settings.php b/includes/settings.php new file mode 100644 index 0000000..6064e31 --- /dev/null +++ b/includes/settings.php @@ -0,0 +1,2 @@ + + + + +

+ + ' . esc_html__('Posted', 'bluesky-connector') . ''; + break; + case 'error': + echo '' . esc_html__('Error', 'bluesky-connector') . ''; + break; + case 'queued': + echo '' . esc_html__('Queued', 'bluesky-connector') . ''; + break; + default: + echo '' . esc_html__('Unknown', 'bluesky-connector') . ''; + } + ?> +

+ + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + +
+ + + +
+ +

+ + + \ No newline at end of file diff --git a/templates/settings-page.php b/templates/settings-page.php new file mode 100644 index 0000000..de98032 --- /dev/null +++ b/templates/settings-page.php @@ -0,0 +1,167 @@ +
+

+ + + +
+

+
+ +
+

+
+ + + +
+

+
+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+
+ + + + + + + + + + + + +
+
+ +
+

+ +

+ +

+ + 0) : ?> +
+ + +
+ +
+ + +
+

+ + + + + + + + + + +
+ + +
\ No newline at end of file