options = get_option( 'ambluesky_options' ); add_action( 'wp_enqueue_scripts', [$this, 'ambluesky_enqueue_stylesheet'], 10 ); add_action( 'publish_post', [ $this, 'post_excerpt_to_bluesky' ], 10, 1 ); } public function getOptions() { return $this->options; } public function getOption($key) { return isset($this->options[$key]) ? $this->options[$key] : null; } public function updateOption($key, $value) { if (is_array($this->options)) { $this->options[$key] = $value; update_option('ambluesky_options', $this->options); } } public function ambluesky_enqueue_stylesheet() { wp_enqueue_style( 'ambluesky-plugin-style', plugin_dir_url( __FILE__ ) . 'includes/bluesky-posts-style.css', false, AMBLUESKY_PLUGIN_VERSION ); } private function authenticate() { $this->username = $this->getOption("username"); $this->app_password = $this->getOption("apppwd"); $auth_data = json_encode([ "identifier" => $this->username, "password" => $this->app_password ]); $auth_response = $this->api_request($this->auth_url, $auth_data); if (!$auth_response || !isset($auth_response['accessJwt']) || !isset($auth_response['did'])) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log("Bluesky auth failed: " . json_encode($auth_response)); } return false; } $this->access_token = $auth_response['accessJwt']; $this->user_did = $auth_response['did']; return true; } public function insertHyperlinks($text, $facets) { if (!$facets || !is_array($facets)) { return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); } // Sort facets by index position to avoid incorrect replacements usort($facets, function ($a, $b) { return $b['index']['byteStart'] - $a['index']['byteStart']; }); // Process facets to insert hyperlinks foreach ($facets as $facet) { if (isset($facet['features'])) { foreach ($facet['features'] as $feature) { if ($feature['$type'] === 'app.bsky.richtext.facet#link' && isset($feature['uri'])) { $url = htmlspecialchars($feature['uri'], ENT_QUOTES, 'UTF-8'); $start = $facet['index']['byteStart']; $end = $facet['index']['byteEnd']; $text = html_entity_decode($text); $linkText = mb_substr($text, $start, $end - $start, 'UTF-8'); $linkHtml = '' . htmlspecialchars($linkText, ENT_QUOTES, 'UTF-8') . ''; $text = mb_substr($text, 0, $start, 'UTF-8') . $linkHtml . mb_substr($text, $end, null, 'UTF-8'); } } } } return $text; } /** * Post excerpt to Bluesky when a WordPress post is published * Premium feature only * * @param int $post_id Post ID */ public function post_excerpt_to_bluesky($post_id) { // Check if feature is enabled if ($this->getOption("postExcerpt") != 1) { return; } // Prevent duplicate posts during autosave if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // Check if this is a revision if (wp_is_post_revision($post_id)) { return; } // Get post object $post = get_post($post_id); // Only proceed for published posts if ($post->post_status !== 'publish') { return; } // Check if already posted to Bluesky (prevent duplicate posts on update) $already_posted = get_post_meta($post_id, '_bluesky_posted', true); if ($already_posted) { return; } // Authenticate if (!$this->authenticate()) { error_log('Bluesky: Authentication failed for post ID ' . $post_id); return; } // Get and clean excerpt $excerpt = trim(wp_strip_all_tags(get_the_excerpt($post_id))); $link = get_permalink($post_id); // Calculate max lengths $max_total_length = 300; $link_len = strlen($link) + 3; // +3 for ' | ' $max_excerpt_length = $max_total_length - $link_len; // Truncate excerpt if necessary if (mb_strlen($excerpt) > $max_excerpt_length) { $excerpt = mb_substr($excerpt, 0, $max_excerpt_length - 1) . '…'; } $final_text = $excerpt . ' | ' . $link; // Prepare post data $post_data = [ 'repo' => $this->user_did, 'collection' => 'app.bsky.feed.post', 'record' => [ '$type' => 'app.bsky.feed.post', 'text' => $final_text, 'createdAt' => gmdate('c'), ], ]; // Send to Bluesky $post_response = wp_remote_post('https://bsky.social/xrpc/com.atproto.repo.createRecord', [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->access_token, ], 'body' => json_encode($post_data), ]); // Handle response if (is_wp_error($post_response)) { error_log('Bluesky post failed for post ID ' . $post_id . ': ' . $post_response->get_error_message()); return; } $response_body = wp_remote_retrieve_body($post_response); $response_data = json_decode($response_body, true); if (!empty($response_data['uri'])) { // Mark post as shared to prevent duplicates update_post_meta($post_id, '_bluesky_posted', true); update_post_meta($post_id, '_bluesky_uri', $response_data['uri']); error_log('Bluesky post successful for post ID ' . $post_id . ': ' . $response_data['uri']); } else { error_log('Bluesky post may have failed for post ID ' . $post_id . ': ' . $response_body); } } public function amBluesky_main($atts = []) { $limit = 5; if (array_key_exists("username", $atts)) { $this->username = $atts['username']; } if (array_key_exists("appwd", $atts)) { $this->app_password = $atts['appwd']; } if (array_key_exists("apppwd", $atts)) { $this->app_password = $atts['apppwd']; } if (array_key_exists("limit", $atts)) { if (is_numeric($atts['limit'])) { $limit = $atts['limit']; } } if (!$this->authenticate()) { return '
Unable to connect to Bluesky. Please check your credentials.
'; } $feed_response = $this->api_request("$this->feed_url?actor=$this->username&limit=$limit", null, $this->access_token); if (!$feed_response || !isset($feed_response['feed'][0]['post'])) { return sprintf('No posts found for user %s.
', esc_html($this->username)); } $authorDisplayName = $feed_response['feed'][0]['post']['author']['displayName']; $h = ""; $h .= "Thank you for using Airdrie Media posts for Bluesky! For more information on this plugin, please see the documentation.
The only required settings are Bluesky User Name and Bluesky App Password. The rest of the controls affect the display of the posts. See documentation for details on all of the settings.
getOption($key); ?> /> render_checkbox('showDisplayName', 'Show display name of account'); } public function cb_showReplies_callback() { $this->render_checkbox('showReplies', 'Show "reply" count'); } public function cb_showReposts_callback() { $this->render_checkbox('showReposts', 'Show "repost" count'); } public function cb_showQuotes_callback() { $this->render_checkbox('showQuotes', 'Show "quote" count'); } public function cb_makeLinks_callback() { $this->render_checkbox('makeLinks', 'Make links active'); } public function cb_showLikes_callback() { $setVal = $this->getOption('showLikes'); ?> /> render_checkbox('showImages', 'Show images in posts'); } public function cb_postExcerpt_callback() { $setVal = $this->getOption('postExcerpt'); ?> />