bluesky-Connector/bluesky-connector.php

805 lines
26 KiB
PHP
Raw Normal View History

2024-11-21 09:05:37 +00:00
<?php
/*
Plugin Name: Bluesky Publisher for WordPress
Description: A WordPress plugin for publishing posts to Bluesky with customizable formatting, image handling, and queue management.
Version: 1.0.0
Author: Eugene Web Doctor
Author URI: https://eugenewebdoctor.com
License: MIT
License URI: https://opensource.org/licenses/MIT
Text Domain: bluesky-connector
Domain Path: /languages
Copyright (c) 2024 Eugene Web Doctor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
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.
*/
// Prevent direct file access
if (!defined('ABSPATH')) {
exit;
}
// Define constants
define('BLUESKY_CONNECTOR_DIR', plugin_dir_path(__FILE__));
define('BLUESKY_CONNECTOR_URL', plugin_dir_url(__FILE__));
define('BLUESKY_CONNECTOR_VERSION', '1.0.0');
// Include necessary files
require_once BLUESKY_CONNECTOR_DIR . 'includes/bluesky-api.php';
require_once BLUESKY_CONNECTOR_DIR . 'includes/bluesky-auth.php';
require_once BLUESKY_CONNECTOR_DIR . 'includes/post-formatter.php';
require_once BLUESKY_CONNECTOR_DIR . 'includes/settings.php';
/**
* Main plugin class
*/
class Bluesky_Connector
{
/**
* Plugin instance
*
* @var Bluesky_Connector|null
*/
private static $instance = null;
/**
* Get plugin instance
*
* @return Bluesky_Connector
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct()
{
// Initialize plugin
add_action('init', array($this, 'init'));
add_action('init', array($this, 'ajax_init'));
// Admin hooks
if (is_admin()) {
add_action('admin_init', array($this, 'admin_init'));
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_notices', array($this, 'admin_notices'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_filter(
'plugin_action_links_' . plugin_basename(BLUESKY_CONNECTOR_DIR . 'bluesky-connector.php'),
array($this, 'add_settings_link')
);
}
// Post hooks
add_action('publish_post', array($this, 'handle_post_publish'), 10, 2);
add_action('process_bluesky_post_queue', array($this, 'process_post_queue'));
add_action('add_meta_boxes', array($this, 'add_post_meta_box'));
add_action('save_post', array($this, 'save_post_meta_box'));
// Plugin activation/deactivation
register_activation_hook(BLUESKY_CONNECTOR_DIR . 'bluesky-connector.php', array($this, 'activate'));
register_deactivation_hook(BLUESKY_CONNECTOR_DIR . 'bluesky-connector.php', array($this, 'deactivate'));
}
/**
* Initialize plugin
*/
public function init()
{
load_plugin_textdomain('bluesky-connector', false, dirname(plugin_basename(__FILE__)) . '/languages');
if (!wp_next_scheduled('process_bluesky_post_queue')) {
wp_schedule_event(time(), 'hourly', 'process_bluesky_post_queue');
}
}
/**
* Initialize AJAX handlers
*/
public function ajax_init()
{
add_action('wp_ajax_bluesky_retry_post', array($this, 'handle_retry_post'));
add_action('wp_ajax_bluesky_share_post', array($this, 'handle_share_post'));
}
/**
* Initialize admin settings
*/
public function admin_init()
{
// Register settings
register_setting('bluesky_connector_settings', 'bluesky_domain', array(
'type' => '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 '<p>' . esc_html__('Configure your Bluesky connection settings below.', 'bluesky-connector') . '</p>';
}
/**
* Render domain field
*/
public function render_domain_field()
{
$value = get_option('bluesky_domain', 'https://bsky.social');
echo '<input type="url" name="bluesky_domain" value="' . esc_attr($value) . '" class="regular-text">';
echo '<p class="description">' . esc_html__('The Bluesky API domain (default: https://bsky.social)', 'bluesky-connector') . '</p>';
}
/**
* Render identifier field
*/
public function render_identifier_field()
{
$value = get_option('bluesky_identifier', '');
echo '<input type="text" name="bluesky_identifier" value="' . esc_attr($value) . '" class="regular-text">';
echo '<p class="description">' . esc_html__('Your Bluesky handle (e.g., username.bsky.social)', 'bluesky-connector') . '</p>';
}
/**
* Render password field
*/
public function render_password_field()
{
echo '<input type="password" name="bluesky_password" class="regular-text">';
echo '<p class="description">' . esc_html__('Your Bluesky app password (will not be stored)', 'bluesky-connector') . '</p>';
}
/**
* 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(
'<a href="%s">%s</a>',
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 '<p>' . esc_html__('Customize how your posts appear on Bluesky.', 'bluesky-connector') . '</p>';
}
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 '<select name="bluesky_post_format" id="bluesky_post_format">';
foreach ($options as $value => $label) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr($value),
selected($format, $value, false),
esc_html($label)
);
}
echo '</select>';
echo '<p class="description">' . esc_html__('Choose how you want your posts to be formatted on Bluesky.', 'bluesky-connector') . '</p>';
}
public function render_include_title_field() {
$include_title = get_option('bluesky_include_title', true);
printf(
'<label><input type="checkbox" name="bluesky_include_title" value="1" %s> %s</label>',
checked($include_title, true, false),
__('Include post title at the beginning', 'bluesky-connector')
);
echo '<p class="description">' . esc_html__('Add the post title to the beginning of each Bluesky post.', 'bluesky-connector') . '</p>';
}
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 '<select name="bluesky_title_separator">';
foreach ($options as $value => $label) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr($value),
selected($separator, $value, false),
esc_html($label)
);
}
echo '</select>';
echo '<p class="description">' . esc_html__('Choose how to separate the title from the excerpt.', 'bluesky-connector') . '</p>';
}
}
/**
* 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_%'");
}