diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..679dc4f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "direnv.path.executable": "/usr/bin/direnv" +} \ No newline at end of file diff --git a/README.md b/README.md index 88d64bf..c698709 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,98 @@ -# Bluesky Publisher for WordPress +=== Share On Bluesky === +Contributors: eugenewebdoctor +Tags: bluesky, social media, cross-posting, atproto +Requires at least: 5.0 +Tested up to: 6.7 +Requires PHP: 7.4 +Stable tag: 1.0.0 +License: MIT +License URI: https://opensource.org/licenses/MIT -Automatically share your WordPress posts to Bluesky with customizable formatting, image handling, and queue management. +A simple WordPress plugin for automatically sharing your posts to Bluesky with support for featured images and customizable formatting. -## Description +== Description == -Bluesky Publisher for WordPress enables automatic cross-posting of your WordPress content to Bluesky. With customizable post formatting, image support, and reliable queue management, you can ensure your content looks great on Bluesky. +Share On Bluesky is a lightweight WordPress plugin that enables automatic cross-posting to Bluesky. When you publish a post, it automatically shares it to your Bluesky account with proper formatting and image support. -### Features += Key Features = -* Automatic post sharing to Bluesky -* Customizable post formatting -* Featured image support with automatic resizing -* Queue management for reliable posting -* Post meta box for post status and manual controls -* Support for post titles, excerpts, and links -* Proper spacing and formatting of posts -* Automatic token refresh +* One-click connection to Bluesky using your handle and app password +* Automatic post sharing when you publish +* Featured image support with auto-resizing +* Customizable post format with title and excerpt options +* Manual post/repost controls from post editor +* Secure token management with automatic refresh -## Requirements +== Installation == -- WordPress 5.0 or higher -- PHP 7.4 or higher -- Bluesky account +There are two ways to install the Share On Bluesky plugin: -## Installation += From WordPress Dashboard (Recommended) = -1. Upload 'bluesky-connector' folder to the '/wp-content/plugins/' directory -2. Activate the plugin through the 'Plugins' menu in WordPress -3. Go to Settings > Bluesky Publisher to configure your connection +1. Go to your WordPress Dashboard > Plugins > Add New +2. Search for "Share On Bluesky" +3. Click "Install Now" next to the Share On Bluesky plugin +4. After installation completes, click "Activate" +5. Go to Settings > Bluesky to configure your connection -## Configuration += Manual Installation = -1. Get your Bluesky app password -2. Enter your Bluesky handle and app password in the settings -3. Choose your preferred post format -4. Start publishing! +1. Download the 'share-on-bluesky' plugin from WordPress.org +2. Go to your WordPress Dashboard > Plugins > Add New > Upload Plugin +3. Choose the downloaded zip file and click "Install Now" +4. After installation completes, click "Activate" +5. Go to Settings > Bluesky to configure your connection -## Frequently Asked Questions += After Installation = -### Where do I find my Bluesky app password? +1. Enter your Bluesky handle (username.bsky.social) +2. Generate and enter an app password from your Bluesky account settings +3. Choose your preferred post format options +4. Test by publishing a new post -You can generate an app password in your Bluesky account settings under "App Passwords". +== Frequently Asked Questions == -### What happens if an error occurs during posting? += Where do I find my Bluesky app password? = -Posts are added to a queue and the plugin will automatically retry failed posts. You can also manually retry posts from the post editor. +You can generate an app password in your Bluesky account settings under "App Passwords". Never use your main account password. -## Changelog += How are images handled? = -### 1.0.0 +The plugin automatically uploads your post's featured image to Bluesky when sharing. Images are resized if needed to meet Bluesky's size limits. + += Can I manually control what gets posted? = + +Yes! Each post has a Bluesky status box where you can manually share, retry, or repost content. + +== Screenshots == + +1. Settings page showing connection and format options +2. Post editor integration with Bluesky status and controls + +== Changelog == + += 1.0.0 = * Initial release +* Automatic post sharing with featured images * Customizable post formatting -* Image support with automatic resizing -* Queue management system -* Post meta box controls -* Token refresh functionality +* Manual post controls +* Secure token management -## License +== Privacy Policy == -This project is licensed under the MIT License - see below for details: +This plugin connects to Bluesky's servers (bsky.social) to share your posts. It stores: +* Your Bluesky handle +* Authentication tokens (securely encrypted) +* Post sharing status metadata + +No other personal data is collected or shared. + +== Credits == + +Developed by [Eugene Web Doctor](https://eugenewebdoctor.com) + +== License == -``` MIT License Copyright (c) 2024 Eugene Web Doctor @@ -82,23 +114,13 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` -## Credits - -Developed by [Eugene Web Doctor](https://eugenewebdoctor.com) - -## Support Development +== Support Development == If you find this plugin useful, consider supporting its development: -### Lightning Network -``` -enki@zap.sovbit.host -``` +Lightning Network: +`enki@zap.sovbit.host` -### On-Chain Bitcoin -``` -bc1pe60ykxhl6h8j6w7dpwrn7qzcyay6l52dkfeulkgg72eezgmms3wss3ul42 - -``` \ No newline at end of file +On-Chain Bitcoin: +`bc1pe60ykxhl6h8j6w7dpwrn7qzcyay6l52dkfeulkgg72eezgmms3wss3ul42` \ No newline at end of file diff --git a/assets/css/admin.css b/assets/css/admin.css index eefa9e2..fe77bf1 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -1,4 +1,3 @@ - .bluesky-post-status { padding: 10px; } @@ -36,4 +35,22 @@ .bluesky-share-post { width: 100%; text-align: center; +} + +/* Button States */ +.updating-message { + position: relative; + padding-left: 24px !important; +} + +.updating-message:before { + content: ''; + position: absolute; + top: 50%; + left: 8px; + margin-top: -8px; + width: 16px; + height: 16px; + background: url(../images/spinner.gif) no-repeat center; + background-size: 16px 16px; } \ No newline at end of file diff --git a/assets/js/admin.js b/assets/js/admin.js index b744142..928157e 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,63 +1,146 @@ jQuery(document).ready(function($) { - // Retry posting to Bluesky - $('.bluesky-retry-post').on('click', function(e) { - e.preventDefault(); - var button = $(this); - var postId = button.data('post-id'); - - button.prop('disabled', true); - - $.ajax({ - url: ajaxurl, - type: 'POST', - data: { - action: 'bluesky_retry_post', - post_id: postId, - nonce: blueskyAdmin.nonce - }, - success: function(response) { - if (response.success) { - location.reload(); - } else { - alert(response.data.message || 'Error retrying post'); - button.prop('disabled', false); - } - }, - error: function() { - alert('Network error. Please try again.'); - button.prop('disabled', false); - } - }); - }); + // Post status container element + const $statusContainer = $('.bluesky-post-status'); - // Share post to Bluesky + // Helper function to display status messages + function updateStatus(message, type = 'info') { + const $status = $(`
${message}
' . esc_html__('Configure your Bluesky connection settings.', 'bluesky-connctor') . '
'; + } + + public function render_format_section() + { + echo '' . esc_html__('Configure how your posts appear on Bluesky.', 'bluesky-connctor') . '
'; } - + /** + * Enqueue admin scripts and styles + * + * @param string $hook The current admin page. + */ + public function enqueue_admin_scripts($hook) { + // Only load on post edit screens and settings page + if (!in_array($hook, array('post.php', 'post-new.php', 'settings_page_bluesky-settings'))) { + return; + } + + // Enqueue WordPress media uploader + + 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( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'strings' => array( + 'selectImage' => esc_html__('Select Fallback Image', 'bluesky-connctor'), + 'useImage' => esc_html__('Use this image', 'bluesky-connctor'), + 'publishing' => esc_html__('Publishing...', 'bluesky-connctor'), + 'retrying' => esc_html__('Retrying...', 'bluesky-connctor'), + 'reposting' => esc_html__('Reposting...', 'bluesky-connctor'), + 'retry' => esc_html__('Retry Post', 'bluesky-connctor'), + 'repost' => esc_html__('Post Again', 'bluesky-connctor'), + 'error' => esc_html__('An error occurred. Please try again.', 'bluesky-connctor'), + 'confirmRepost' => esc_html__('Are you sure you want to post this content again?', 'bluesky-connctor') + ), + 'nonce' => wp_create_nonce('bluesky_post_now'), + )); + + wp_enqueue_style( + 'bluesky-admin', + BLUESKY_CONNECTOR_URL . 'assets/css/admin.css', + array(), + BLUESKY_CONNECTOR_VERSION + ); + } /** - * Add admin menu + * Handle post saving and immediate posting to Bluesky + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ + public function handle_post_save($post_id, $post) + { + error_log('[Bluesky Connector] handle_post_save called for post ' . $post_id); + + // Skip autosaves + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + error_log('[Bluesky Connector] Skipping - autosave'); + return; + } + + // Check post type + if ('post' !== $post->post_type) { + error_log('[Bluesky Connector] Skipping - not a post: ' . $post->post_type); + return; + } + + // Only proceed for published posts + if ('publish' !== $post->post_status) { + error_log('[Bluesky Connector] Skipping - not published: ' . $post->post_status); + return; + } + + $already_posted = get_post_meta($post_id, '_bluesky_posted', true); + $had_image = get_post_meta($post_id, '_bluesky_had_image', true); + $has_image_now = has_post_thumbnail($post->ID); + + error_log('[Bluesky Connector] Post status check:'); + error_log('[Bluesky Connector] - Already posted: ' . ($already_posted ? 'yes' : 'no')); + error_log('[Bluesky Connector] - Had image: ' . ($had_image ? 'yes' : 'no')); + error_log('[Bluesky Connector] - Has image now: ' . ($has_image_now ? 'yes' : 'no')); + + // If already posted, only proceed if image status has changed + if ($already_posted) { + if ($had_image === $has_image_now) { + error_log('[Bluesky Connector] Skipping - already posted and image status unchanged'); + return; + } + error_log('[Bluesky Connector] Image status changed, proceeding with update'); + } + + // Only check nonce if this is a manual save from the editor + if (isset($_POST['bluesky_post_meta_box_nonce'])) { + if (!wp_verify_nonce( + sanitize_text_field(wp_unslash($_POST['bluesky_post_meta_box_nonce'])), + 'bluesky_post_meta_box' + )) { + error_log('[Bluesky Connector] Skipping - invalid nonce on manual save'); + return; + } + } + + error_log('[Bluesky Connector] Calling post_to_bluesky'); + + // Post to Bluesky + $response = $this->post_to_bluesky($post); + + // If successful, update image status + if (!isset($response['error'])) { + update_post_meta($post_id, '_bluesky_had_image', $has_image_now); + } +} + + /** + * Post content to Bluesky + * + * @param WP_Post $post Post object. + * @return array Response from Bluesky API. + */ + private function post_to_bluesky($post) + { + try { + error_log('[Bluesky Connector] Starting post_to_bluesky for post ' . $post->ID); + update_post_meta($post->ID, '_bluesky_status', 'pending'); + + $identifier = get_option('bluesky_identifier'); + $password = get_option('bluesky_password'); + + if (empty($identifier) || empty($password)) { + throw new Exception('Bluesky identifier or password is missing.'); + } + + // Log settings (without password) + error_log('[Bluesky Connector] Using identifier: ' . $identifier); + + // Authenticate and get access token + $auth = new Bluesky_Auth($identifier, $password); + $token = $auth->get_access_token(); + + if (isset($token['error'])) { + throw new Exception($token['error']); + } + + // Create Post_Formatter instance + $formatter = new Post_Formatter($token, get_option('bluesky_did')); + $response = $formatter->format_and_post($post); + + // Update meta data if successful + if (!isset($response['error'])) { + $record_key = $response['uri']; + if (strpos($response['uri'], 'at://') === 0) { + $parts = explode('/', $response['uri']); + $record_key = end($parts); + } + + update_post_meta($post->ID, '_bluesky_posted', current_time('mysql')); + update_post_meta($post->ID, '_bluesky_post_id', sanitize_text_field($record_key)); + update_post_meta($post->ID, '_bluesky_status', 'success'); + update_option('bluesky_last_post_time', time()); + } + + return $response; + + } catch (Exception $e) { + update_post_meta($post->ID, '_bluesky_status', 'error'); + update_post_meta($post->ID, '_bluesky_error', sanitize_text_field($e->getMessage())); + $this->log_error('Bluesky posting failed: ' . $e->getMessage()); + return array('error' => $e->getMessage()); + } + } + + /** + * Log error message if debug mode is enabled + * + * @param string $message Error message to log. + * @param array $data Optional data to log. + */ + // In bluesky-connector.php, inside the Bluesky_Connector class + private function log_error($message, $data = array()) + { + if (defined('WP_DEBUG') && WP_DEBUG) { + if (is_array($data) || is_object($data)) { + $data = wp_json_encode($data); + } + error_log(sprintf('[Bluesky Connector] %s | Data: %s', $message, $data)); + } + } + + /** + * Handle AJAX immediate post request + */ + public function handle_immediate_post() + { + check_ajax_referer('bluesky_post_now', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(array( + 'message' => esc_html__('Permission denied.', 'bluesky-connctor') + )); + } + + $post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT); + if (!$post_id) { + wp_send_json_error(array( + 'message' => esc_html__('Invalid post ID.', 'bluesky-connctor') + )); + } + + $post = get_post($post_id); + if (!$post) { + wp_send_json_error(array( + 'message' => esc_html__('Post not found.', 'bluesky-connctor') + )); + } + + $response = $this->post_to_bluesky($post); + + if (isset($response['error'])) { + wp_send_json_error(array( + 'message' => esc_html($response['error']) + )); + } else { + wp_send_json_success(array( + 'message' => esc_html__('Posted successfully to Bluesky.', 'bluesky-connctor'), + 'post_id' => esc_html($response['uri']) + )); + } + } + + /** + * Add admin menu items */ public function add_admin_menu() { add_options_page( - __('Bluesky Connector Settings', 'bluesky-connector'), - __('Bluesky Connector', 'bluesky-connector'), + esc_html__('Bluesky Settings', 'bluesky-connctor'), + esc_html__('Bluesky', 'bluesky-connctor'), 'manage_options', - 'bluesky-connector', + 'bluesky-settings', array($this, 'render_settings_page') ); } + /** + * Add post meta box + */ + public function add_post_meta_box() + { + add_meta_box( + 'bluesky_post_meta', + esc_html__('Bluesky Status', 'bluesky-connctor'), + array($this, 'render_post_meta_box'), + 'post', + 'side', + 'default' + ); + } + + /** + * Render post meta box + * + * @param WP_Post $post Post object. + */ + public function render_post_meta_box($post) + { + // Add this line at the very start of the function + wp_nonce_field('bluesky_post_meta_box', 'bluesky_post_meta_box_nonce'); + + $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); + + require BLUESKY_CONNECTOR_DIR . 'templates/post-meta-box.php'; + } + + /** + * Add settings link to plugins page + * + * @param array $links Plugin action links. + * @return array Modified plugin action links. + */ + public function add_settings_link($links) + { + $settings_link = sprintf( + '%s', + esc_url(admin_url('options-general.php?page=bluesky-settings')), + esc_html__('Settings', 'bluesky-connctor') + ); + + array_unshift($links, $settings_link); + return $links; + } + /** * Render settings page */ @@ -262,358 +614,13 @@ class Bluesky_Connector 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', ''), + 'identifier' => esc_html(get_option('bluesky_identifier', '')), 'connection_status' => get_option('bluesky_connection_status', ''), - 'last_error' => get_option('bluesky_last_error', '') + 'last_error' => esc_html(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'); - } + require BLUESKY_CONNECTOR_DIR . 'templates/settings-page.php'; } /** @@ -626,7 +633,7 @@ class Bluesky_Connector } $screen = get_current_screen(); - if ($screen->id !== 'settings_page_bluesky-connector') { + if ($screen->id !== 'settings_page_bluesky-settings') { return; } @@ -634,172 +641,103 @@ class Bluesky_Connector } /** - * Add settings link to plugins page + * Plugin activation */ - public function add_settings_link($links) + public function activate() { - $settings_link = sprintf( - '%s', - admin_url('options-general.php?page=bluesky-connector'), - __('Settings', 'bluesky-connector') - ); - array_unshift($links, $settings_link); - return $links; + // Create necessary options with default values + add_option('bluesky_domain', BLUESKY_API_DOMAIN); + add_option('bluesky_post_format', 'title-excerpt-link'); + + // Ensure languages directory exists + $languages_dir = dirname(plugin_basename(__FILE__)) . '/languages/'; + if (!file_exists($languages_dir)) { + wp_mkdir_p($languages_dir); + } + + // Clear any existing caches + wp_cache_delete('bluesky_settings', 'options'); } /** - * Log plugin errors + * Plugin deactivation */ - private function log_error($message, $data = array()) + public function deactivate() { - if (defined('WP_DEBUG') && WP_DEBUG) { - error_log(sprintf( - '[Bluesky Connector] %s | Data: %s', - $message, - print_r($data, true) - )); - } - } + // Clean up transients and caches + delete_transient('bluesky_auth_check'); + wp_cache_delete('bluesky_settings', 'options'); - /** - * 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') . '
'; + // Log deactivation if in debug mode + $this->log_error('Plugin deactivated'); } } /** - * 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 + * Clean up plugin data + * Used during 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 all plugin options + $options = array( + 'bluesky_domain', + 'bluesky_identifier', + 'bluesky_password', + 'bluesky_access_jwt', + 'bluesky_refresh_jwt', + 'bluesky_did', + 'bluesky_post_queue', + 'bluesky_connection_status', + 'bluesky_last_error', + 'bluesky_post_format', + 'bluesky_connector_settings', + 'bluesky_token_created', + 'bluesky_last_post_time' + ); - // Remove capabilities - $role = get_role('administrator'); - if ($role) { - $role->remove_cap('manage_bluesky_settings'); + foreach ($options as $option) { + delete_option($option); } - // Clear scheduled hooks - wp_clear_scheduled_hook('process_bluesky_post_queue'); + // Remove all post meta + $meta_keys = array( + '_bluesky_posted', + '_bluesky_status', + '_bluesky_error', + '_bluesky_post_id', + '_bluesky_had_image' // Added new meta key + ); - // Remove post meta - global $wpdb; - $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_bluesky_%'"); -} \ No newline at end of file + // Get all posts + $posts = get_posts(array( + 'posts_per_page' => -1, + 'post_type' => 'any', + 'fields' => 'ids' + )); + + // Delete meta and clear cache for each post + foreach ($posts as $post_id) { + foreach ($meta_keys as $meta_key) { + delete_post_meta($post_id, $meta_key); + } + clean_post_cache($post_id); + } + + // Clear settings cache + wp_cache_delete('bluesky_settings', 'options'); +} + +// Initialize plugin +function bluesky_connector_init() +{ + return Bluesky_Connector::get_instance(); +} +add_action('plugins_loaded', 'bluesky_connector_init'); + +// Register uninstall hook +register_uninstall_hook(__FILE__, 'bluesky_connector_uninstall'); \ No newline at end of file diff --git a/includes/bluesky-api.php b/includes/bluesky-api.php index 213e096..47a6110 100644 --- a/includes/bluesky-api.php +++ b/includes/bluesky-api.php @@ -1,15 +1,18 @@ api_key = $api_key; $this->did = $did; + error_log('[Bluesky Connector] Initializing API with DID: ' . $did); } public function create_post($post_data) { + error_log('[Bluesky Connector] Creating post with data: ' . wp_json_encode($post_data)); + $headers = array( 'Authorization' => 'Bearer ' . $this->api_key, 'Content-Type' => 'application/json', @@ -17,54 +20,96 @@ class Bluesky_API { $response = wp_remote_post($this->api_url . '/com.atproto.repo.createRecord', array( 'headers' => $headers, - 'body' => json_encode(array( + 'body' => wp_json_encode(array( 'repo' => $this->did, 'collection' => 'app.bsky.feed.post', 'record' => $post_data, )), + 'timeout' => 30, )); if (is_wp_error($response)) { + error_log('[Bluesky Connector] Create post error: ' . $response->get_error_message()); return array('error' => $response->get_error_message()); } - return json_decode(wp_remote_retrieve_body($response), true); + $response_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('[Bluesky Connector] Create post response code: ' . $response_code); + error_log('[Bluesky Connector] Create post response: ' . $response_body); + + return json_decode($response_body, true); } public function resolve_handle($handle) { + error_log('[Bluesky Connector] Resolving handle: ' . $handle); + $response = wp_remote_get($this->api_url . '/com.atproto.identity.resolveHandle', array( 'query' => array( 'handle' => $handle, ), + 'timeout' => 30, )); if (is_wp_error($response)) { + error_log('[Bluesky Connector] Resolve handle error: ' . $response->get_error_message()); return array('error' => $response->get_error_message()); } return json_decode(wp_remote_retrieve_body($response), true); } - public function upload_blob($file_path, $mime_type) { + public function upload_blob($file_path_or_data, $mime_type) { + error_log('[Bluesky Connector] Starting blob upload'); + error_log('[Bluesky Connector] Mime type: ' . $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.'); + // Determine if input is a file path or raw data + if (is_string($file_path_or_data) && file_exists($file_path_or_data)) { + error_log('[Bluesky Connector] Reading from file path: ' . $file_path_or_data); + $file_contents = file_get_contents($file_path_or_data); + if ($file_contents === false) { + error_log('[Bluesky Connector] Failed to read file'); + return array('error' => 'Failed to read file'); + } + } else { + error_log('[Bluesky Connector] Using provided data directly'); + $file_contents = $file_path_or_data; + } + + // Check content size + $content_size = strlen($file_contents); + error_log('[Bluesky Connector] Content size: ' . $content_size . ' bytes'); + + if ($content_size > 1000000) { + error_log('[Bluesky Connector] Content size exceeds 1MB limit'); + return array('error' => 'File size exceeds 1MB limit'); } $response = wp_remote_post($this->api_url . '/com.atproto.repo.uploadBlob', array( 'headers' => $headers, 'body' => $file_contents, + 'timeout' => 30, )); if (is_wp_error($response)) { + error_log('[Bluesky Connector] Upload blob error: ' . $response->get_error_message()); return array('error' => $response->get_error_message()); } - return json_decode(wp_remote_retrieve_body($response), true); + $response_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('[Bluesky Connector] Upload blob response code: ' . $response_code); + error_log('[Bluesky Connector] Upload blob response: ' . $response_body); + + if ($response_code !== 200) { + return array('error' => 'Upload failed with status ' . $response_code); + } + + return json_decode($response_body, true); } } \ No newline at end of file diff --git a/includes/bluesky-auth.php b/includes/bluesky-auth.php index 11e0d16..34cf611 100644 --- a/includes/bluesky-auth.php +++ b/includes/bluesky-auth.php @@ -20,33 +20,74 @@ class Bluesky_Auth { // 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); + // Log configuration details if debugging is enabled + $this->log_debug('Auth Configuration', array( + 'domain_setting' => get_option('bluesky_domain'), + 'processed_domain' => $this->api_domain, + 'identifier' => $this->identifier + )); } public function get_access_token() { + // First check if we have a valid access token + $access_token = get_option('bluesky_access_jwt'); + $token_created = get_option('bluesky_token_created'); + + if (!empty($access_token) && $token_created > (time() - 7200)) { + $this->log_debug('Using existing valid token'); + return $access_token; + } + + // If we have a refresh token, try to use it if ($this->should_refresh_token()) { - return $this->refresh_access_token(); + $refresh_result = $this->refresh_access_token(); + if (!isset($refresh_result['error'])) { + return $refresh_result; + } + } + + // If we got here, we need to create a new session + if (empty($this->identifier) || empty($this->password)) { + // Try to get credentials from settings if not provided + $settings = get_option('bluesky_connector_settings', array()); + if (!empty($settings['identifier'])) { + $this->identifier = $settings['identifier']; + } + if (!empty($settings['password'])) { + $this->password = $settings['password']; + } + + // If still empty, try individual options + if (empty($this->identifier)) { + $this->identifier = get_option('bluesky_identifier'); + } + if (empty($this->password)) { + $this->password = get_option('bluesky_password'); + } + + if (empty($this->identifier) || empty($this->password)) { + $this->log_debug('Missing credentials', array( + 'has_identifier' => !empty($this->identifier), + 'has_password' => !empty($this->password) + )); + return array('error' => 'Missing credentials - please check settings'); + } } $token_url = $this->api_domain . 'xrpc/com.atproto.server.createSession'; - // Debug log - error_log('Bluesky Auth - Attempting connection to: ' . $token_url); + $this->log_debug('Attempting connection', array('url' => $token_url)); $headers = array( 'Content-Type' => 'application/json', ); - $body = json_encode(array( + $body = wp_json_encode(array( 'identifier' => $this->identifier, 'password' => $this->password, )); - // Debug log - error_log('Bluesky Auth - Request body: ' . $body); + $this->log_debug('Request prepared', array('body' => str_replace($this->password, '[REDACTED]', $body))); $wp_version = get_bloginfo('version'); $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); @@ -55,38 +96,50 @@ class Bluesky_Auth { 'headers' => $headers, 'user-agent' => "$user_agent; Bluesky Connector", 'body' => $body, - 'timeout' => 30, // Increase timeout + 'timeout' => 30, )); if (is_wp_error($response)) { $error_message = $response->get_error_message(); - error_log('Bluesky Auth Error: ' . $error_message); + $this->log_debug('Auth Error', array('error' => $error_message)); return array('error' => $error_message); } - // Debug response + // Log response details $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); + $this->log_debug('Response received', array( + 'status' => $status_code, + 'body' => $response_body + )); $data = json_decode($response_body, true); if (!empty($data['accessJwt']) && !empty($data['refreshJwt']) && !empty($data['did'])) { + // Save credentials in both locations + $settings = get_option('bluesky_connector_settings', array()); + $settings['identifier'] = $this->identifier; + update_option('bluesky_connector_settings', $settings); + + // Save all necessary tokens and details + update_option('bluesky_identifier', sanitize_text_field($this->identifier)); 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 + + // Store password for reauth + update_option('bluesky_password', $this->password); + + $this->log_debug('Authentication successful', array('did' => $data['did'])); 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); + $this->log_debug('Auth Failed', array('error' => $error_message)); return array('error' => $error_message); } @@ -94,7 +147,6 @@ class Bluesky_Auth { $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)); } @@ -105,9 +157,7 @@ class Bluesky_Auth { } $refresh_url = $this->api_domain . 'xrpc/com.atproto.server.refreshSession'; - - // Debug log - error_log('Bluesky Auth - Attempting token refresh at: ' . $refresh_url); + $this->log_debug('Token refresh attempt', array('url' => $refresh_url)); $wp_version = get_bloginfo('version'); $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); @@ -123,22 +173,18 @@ class Bluesky_Auth { if (is_wp_error($response)) { $error_message = $response->get_error_message(); - error_log('Bluesky Token Refresh Error: ' . $error_message); + $this->log_debug('Token Refresh Error', array('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); + $data = json_decode(wp_remote_retrieve_body($response), 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()); + + $this->log_debug('Token refresh successful'); return $data['accessJwt']; } @@ -146,7 +192,23 @@ class Bluesky_Auth { if (isset($data['message'])) { $error_message .= ' - ' . $data['message']; } - error_log('Bluesky Auth Refresh - Error: ' . $error_message); + $this->log_debug('Token Refresh Failed', array('error' => $error_message)); return array('error' => $error_message); } + + private function log_debug($message, $data = array()) { + if (defined('WP_DEBUG') && WP_DEBUG) { + $log_message = sprintf( + '[Bluesky Connector] %s | Data: %s', + $message, + wp_json_encode($data, JSON_PRETTY_PRINT) + ); + + if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + error_log($log_message); + } + + do_action('bluesky_connector_debug', $message, $data); + } + } } \ No newline at end of file diff --git a/includes/post-formatter.php b/includes/post-formatter.php index 5ba409a..5cc2cb6 100644 --- a/includes/post-formatter.php +++ b/includes/post-formatter.php @@ -1,243 +1,457 @@ $attachment->ID, + 'post_type' => $attachment->post_type, + 'status' => $attachment->post_status, + 'mime_type' => $attachment->post_mime_type, + 'attached_file' => get_attached_file($thumbnail_id), + 'exists' => file_exists(get_attached_file($thumbnail_id)) + ], true)); + } else { + error_log('[Bluesky Connector] Attachment post not found for ID: ' . $thumbnail_id); + } + } + + return $theme_support && $post_type_support; + } + + public function __construct($access_token, $did) + { + error_log('[Bluesky Connector] Initializing Post_Formatter with DID: ' . $did); $this->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') - ); + public function format_and_post($post, $formatted_content = null) + { + try { + error_log('[Bluesky Connector] Starting format_and_post for post ' . $post->ID); + + // Use provided content if available, otherwise format it + $content = $formatted_content ?: $this->get_formatted_content($post); + error_log('[Bluesky Connector] Formatted content: ' . $content); - // Add facets for the URL - $post_data['facets'] = $this->parse_facets($post); + $post_data = array( + '$type' => 'app.bsky.feed.post', + 'text' => $content, + 'createdAt' => gmdate('c', strtotime($post->post_date_gmt)), + 'langs' => array('en') + ); - // 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; + // Add facets for the URL + $post_data['facets'] = $this->parse_facets($post); + error_log('[Bluesky Connector] Added facets: ' . wp_json_encode($post_data['facets'])); + + // Debug thumbnail support + error_log('[Bluesky Connector] Checking for featured image...'); + error_log('[Bluesky Connector] Post type: ' . $post->post_type); + error_log('[Bluesky Connector] has_post_thumbnail result: ' . (has_post_thumbnail($post->ID) ? 'true' : 'false')); + error_log('[Bluesky Connector] Theme supports thumbnails: ' . (current_theme_supports('post-thumbnails') ? 'true' : 'false')); + error_log('[Bluesky Connector] Post thumbnail meta check:'); + error_log('[Bluesky Connector] _thumbnail_id: ' . get_post_meta($post->ID, '_thumbnail_id', true)); + error_log('[Bluesky Connector] Post status: ' . get_post_status($post->ID)); + + // Handle image embed + if (has_post_thumbnail($post->ID)) { + error_log('[Bluesky Connector] Processing featured image for post ' . $post->ID); + $image_data = $this->handle_featured_image($post->ID); + if (!empty($image_data) && !isset($image_data['error'])) { + $post_data['embed'] = $image_data; + error_log('[Bluesky Connector] Image data added to post: ' . wp_json_encode($image_data)); + } else { + error_log('[Bluesky Connector] Image processing error: ' . ($image_data['error'] ?? 'Unknown error')); + } } - } - return $this->api->create_post($post_data); + error_log('[Bluesky Connector] Sending post data to API: ' . wp_json_encode($post_data)); + $response = $this->api->create_post($post_data); + error_log('[Bluesky Connector] API Response: ' . wp_json_encode($response)); + + return $response; + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in format_and_post: ' . $e->getMessage()); + return array('error' => $e->getMessage()); + } } - 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 - } + private function get_formatted_content($post) + { + try { + error_log('[Bluesky Connector] Starting content formatting for post ' . $post->ID); + + $format = get_option('bluesky_post_format', 'image-title-excerpt-link'); + $include_title = get_option('bluesky_include_title', true); - return $content; + error_log('[Bluesky Connector] Using format: ' . $format . ', Include title: ' . ($include_title ? 'yes' : 'no')); + + // Get individual components + $title = $include_title ? $post->post_title : ''; + $excerpt = $this->get_excerpt($post); + $url = wp_get_shortlink($post->ID); + + error_log('[Bluesky Connector] Content components:'); + error_log('[Bluesky Connector] - Title: ' . $title); + error_log('[Bluesky Connector] - Excerpt length: ' . strlen($excerpt)); + error_log('[Bluesky Connector] - URL: ' . $url); + + // Start building content + $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 URL on its own line + if (!empty($url)) { + $content .= "Continue Reading: " . $url; + } + + error_log('[Bluesky Connector] Final formatted content: ' . $content); + return $content; + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in get_formatted_content: ' . $e->getMessage()); + throw $e; + } } - private function get_excerpt($post) { + private function get_excerpt($post) +{ + try { + error_log('[Bluesky Connector] Getting excerpt for post ' . $post->ID); + $text = wp_strip_all_tags($post->post_excerpt); if (empty($text)) { $text = wp_strip_all_tags($post->post_content); + error_log('[Bluesky Connector] Using post content for excerpt (no excerpt found)'); } - + $url = wp_get_shortlink($post->ID); $include_title = get_option('bluesky_include_title', true); - // Calculate available length accounting for spacing and new lines + // Calculate available length $available_length = $this->max_length; $available_length -= strlen($url); - $available_length -= 2; // Account for \n\n after excerpt + $available_length -= strlen("Continue Reading: "); // Account for the prefix + $available_length -= 4; // Account for \n\n before and after the URL if ($include_title) { $available_length -= strlen($post->post_title); $available_length -= 2; // Account for \n\n after title } - + + error_log('[Bluesky Connector] Available length for excerpt: ' . $available_length); + if (mb_strlen($text) > $available_length) { $text = mb_substr($text, 0, $available_length - 3) . '...'; + error_log('[Bluesky Connector] Excerpt truncated to fit length limit'); } - - 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( + error_log('[Bluesky Connector] Final excerpt length: ' . mb_strlen($text)); + return $text; + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in get_excerpt: ' . $e->getMessage()); + throw $e; + } +} + + private function parse_facets($post) + { + try { + error_log('[Bluesky Connector] Parsing facets for post ' . $post->ID); + + $facets = array(); + $content = $this->get_formatted_content($post); + $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, + ), + ), + ); + error_log('[Bluesky Connector] Added URL facet for: ' . $url); + } + + error_log('[Bluesky Connector] Generated facets: ' . wp_json_encode($facets)); + return $facets; + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in parse_facets: ' . $e->getMessage()); + throw $e; + } + } + + private function handle_featured_image($post_id) { + try { + error_log('[Bluesky Connector] Starting featured image process for post ' . $post_id); + + // Run diagnostics first + $thumbnail_support = $this->diagnose_thumbnail_support($post_id); + if (!$thumbnail_support) { + error_log('[Bluesky Connector] Thumbnail support is not properly configured'); + } + + // Get image ID with multiple fallbacks + $image_id = get_post_thumbnail_id($post_id); + error_log('[Bluesky Connector] Initial image ID from get_post_thumbnail_id: ' . $image_id); + + if (!$image_id) { + // Try direct meta approach + $image_id = get_post_meta($post_id, '_thumbnail_id', true); + error_log('[Bluesky Connector] Image ID from direct meta: ' . $image_id); + + // If still no image, check for fallback + if (!$image_id) { + $image_id = get_option('bluesky_fallback_image'); + error_log('[Bluesky Connector] Using fallback image ID: ' . $image_id); + } + } + + if (!$image_id) { + return array('error' => 'No valid image ID found'); + } + + // Get the image file path + $image_path = get_attached_file($image_id); + error_log('[Bluesky Connector] Image path: ' . ($image_path ?: 'not found')); + + // Verify file exists and is accessible + if (!$image_path || !file_exists($image_path)) { + $upload_dir = wp_upload_dir(); + error_log('[Bluesky Connector] Upload directory information: ' . print_r($upload_dir, true)); + return array('error' => 'Image file not found or inaccessible'); + } + + // Check file permissions + error_log('[Bluesky Connector] File permissions: ' . decoct(fileperms($image_path) & 0777)); + + // Verify mime type + $mime_type = get_post_mime_type($image_id); + error_log('[Bluesky Connector] Mime type: ' . $mime_type); + + // Validate mime type + $allowed_types = array('image/jpeg', 'image/png', 'image/gif'); + if (!in_array($mime_type, $allowed_types)) { + return array('error' => 'Unsupported image type: ' . $mime_type); + } + + // Get image data + $image_data = file_get_contents($image_path); + if ($image_data === false) { + return array('error' => 'Failed to read image file'); + } + + // Check and handle file size + $size = strlen($image_data); + error_log('[Bluesky Connector] Original image size: ' . $size . ' bytes'); + + if ($size > 1000000) { + error_log('[Bluesky Connector] Image exceeds size limit, attempting resize'); + $resized = $this->resize_image($image_path); + + if ($resized) { + error_log('[Bluesky Connector] Image resized successfully'); + $image_path = $resized; + + // Verify new size + $new_size = filesize($resized); + error_log('[Bluesky Connector] New image size: ' . $new_size . ' bytes'); + + if ($new_size > 1000000) { + error_log('[Bluesky Connector] Resized image still too large'); + return array('error' => 'Unable to reduce image size below 1MB'); + } + } else { + error_log('[Bluesky Connector] Image resize failed'); + return array('error' => 'Image resize failed'); + } + } + + // Upload to Bluesky + error_log('[Bluesky Connector] Uploading image to Bluesky'); + $response = $this->api->upload_blob($image_path, $mime_type); + + if (isset($response['error'])) { + error_log('[Bluesky Connector] Upload failed: ' . $response['error']); + return $response; + } + + // Clean up temporary file if it exists + if (isset($resized) && file_exists($resized)) { + unlink($resized); + error_log('[Bluesky Connector] Cleaned up temporary resized file'); + } + + // Get alt text + $alt_text = get_post_meta($image_id, '_wp_attachment_image_alt', true) ?: ''; + error_log('[Bluesky Connector] Using alt text: ' . $alt_text); + + // Prepare final image embed + $image_embed = array( + '$type' => 'app.bsky.embed.images', + 'images' => array( array( - '$type' => 'app.bsky.richtext.facet#link', - 'uri' => $url, + 'alt' => $alt_text, + 'image' => $response['blob'], ), ), ); + + error_log('[Bluesky Connector] Image embed prepared successfully'); + return $image_embed; + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in handle_featured_image: ' . $e->getMessage()); + error_log('[Bluesky Connector] Stack trace: ' . $e->getTraceAsString()); + return array('error' => $e->getMessage()); } - - 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: + private function resize_image($image_path) + { + try { + error_log('[Bluesky Connector] Starting image resize for: ' . $image_path); + + if (!function_exists('imagecreatefrompng')) { + error_log('[Bluesky Connector] GD library not available'); return false; - } + } - if (!$source) { + $mime_type = mime_content_type($image_path); + list($width, $height) = getimagesize($image_path); + error_log('[Bluesky Connector] Original dimensions: ' . $width . 'x' . $height); + + // Calculate new dimensions + $max_dimension = 1000; + 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)); + } + + error_log('[Bluesky Connector] New dimensions: ' . $new_width . 'x' . $new_height); + + $new_image = imagecreatetruecolor($new_width, $new_height); + + switch ($mime_type) { + case 'image/jpeg': + $source = imagecreatefromjpeg($image_path); + break; + case 'image/png': + $source = imagecreatefrompng($image_path); + imagealphablending($new_image, false); + imagesavealpha($new_image, true); + break; + case 'image/gif': + $source = imagecreatefromgif($image_path); + break; + default: + error_log('[Bluesky Connector] Unsupported image type: ' . $mime_type); + return false; + } + + if (!$source) { + error_log('[Bluesky Connector] Failed to create image resource'); + return false; + } + + imagecopyresampled( + $new_image, + $source, + 0, 0, 0, 0, + $new_width, + $new_height, + $width, + $height + ); + + // Use PHP's tempnam + $temp_file = tempnam(sys_get_temp_dir(), 'bluesky_img_'); + error_log('[Bluesky Connector] Created temp file: ' . $temp_file); + + $success = false; + switch ($mime_type) { + case 'image/jpeg': + $success = imagejpeg($new_image, $temp_file, 85); + break; + case 'image/png': + $success = imagepng($new_image, $temp_file, 8); + break; + case 'image/gif': + $success = imagegif($new_image, $temp_file); + break; + } + + imagedestroy($source); + imagedestroy($new_image); + + if ($success) { + error_log('[Bluesky Connector] Image resized successfully'); + return $temp_file; + } else { + error_log('[Bluesky Connector] Failed to save resized image'); + return false; + } + + } catch (Exception $e) { + error_log('[Bluesky Connector] Error in resize_image: ' . $e->getMessage()); 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/templates/post-meta-box.php b/templates/post-meta-box.php index 3d6ff94..3f79e77 100644 --- a/templates/post-meta-box.php +++ b/templates/post-meta-box.php @@ -1,58 +1,64 @@- + ' . esc_html__('Posted', 'bluesky-connector') . ''; + echo '' . esc_html__('Posted', 'bluesky-connctor') . ''; break; case 'error': - echo '' . esc_html__('Error', 'bluesky-connector') . ''; + echo '' . esc_html__('Error', 'bluesky-connctor') . ''; break; - case 'queued': - echo '' . esc_html__('Queued', 'bluesky-connector') . ''; + case 'pending': + echo '' . esc_html__('Publishing...', 'bluesky-connctor') . ''; break; default: - echo '' . esc_html__('Unknown', 'bluesky-connector') . ''; + echo '' . esc_html__('Not Posted', 'bluesky-connctor') . ''; } ?>
- - +- +
- - +
-
-
+
+
+ ↗
+
- -
- - 0) : ?> - - -- |
|
- ||
---|---|---|---|
- | + | + | + array(), 'span' => array('class' => array())) + ); + ?> + |
+ |
|
+ ||
+ | + |