Compare commits
No commits in common. "main" and "master" have entirely different histories.
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Enki
|
||||
|
||||
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.
|
105
README.md
105
README.md
@ -1,3 +1,104 @@
|
||||
# bluesky-Connector
|
||||
# Bluesky Publisher for WordPress
|
||||
|
||||
A WordPress plugin for publishing posts to Bluesky with customizable formatting, image handling, and queue management.
|
||||
Automatically share your WordPress posts to Bluesky with customizable formatting, image handling, and queue management.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress 5.0 or higher
|
||||
- PHP 7.4 or higher
|
||||
- Bluesky account
|
||||
|
||||
## Installation
|
||||
|
||||
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
|
||||
|
||||
## Configuration
|
||||
|
||||
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!
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### Where do I find my Bluesky app password?
|
||||
|
||||
You can generate an app password in your Bluesky account settings under "App Passwords".
|
||||
|
||||
### What happens if an error occurs during posting?
|
||||
|
||||
Posts are added to a queue and the plugin will automatically retry failed posts. You can also manually retry posts from the post editor.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 1.0.0
|
||||
* Initial release
|
||||
* Customizable post formatting
|
||||
* Image support with automatic resizing
|
||||
* Queue management system
|
||||
* Post meta box controls
|
||||
* Token refresh functionality
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see below for details:
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Developed by [Eugene Web Doctor](https://eugenewebdoctor.com)
|
||||
|
||||
## Support Development
|
||||
|
||||
If you find this plugin useful, consider supporting its development:
|
||||
|
||||
### Lightning Network
|
||||
```
|
||||
enki@zap.sovbit.host
|
||||
```
|
||||
|
||||
### On-Chain Bitcoin
|
||||
```
|
||||
bc1pe60ykxhl6h8j6w7dpwrn7qzcyay6l52dkfeulkgg72eezgmms3wss3ul42
|
||||
|
||||
```
|
2
admin/settings.php
Normal file
2
admin/settings.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// This file is included in the main plugin file, so no need to include it separately
|
39
assets/css/admin.css
Normal file
39
assets/css/admin.css
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
.bluesky-post-status {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.bluesky-status-success {
|
||||
color: #46b450;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bluesky-status-error {
|
||||
color: #dc3232;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bluesky-status-pending {
|
||||
color: #ffb900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bluesky-error-message {
|
||||
color: #dc3232;
|
||||
margin: 10px 0;
|
||||
padding: 5px;
|
||||
background: #fff8f8;
|
||||
border-left: 4px solid #dc3232;
|
||||
}
|
||||
|
||||
.bluesky-actions {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.bluesky-retry-post,
|
||||
.bluesky-share-post {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
63
assets/js/admin.js
Normal file
63
assets/js/admin.js
Normal file
@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Share post to Bluesky
|
||||
$('.bluesky-share-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_share_post',
|
||||
post_id: postId,
|
||||
nonce: blueskyAdmin.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(response.data.message || 'Error sharing post');
|
||||
button.prop('disabled', false);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Network error. Please try again.');
|
||||
button.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
BIN
bluesky-connctor.zip
Normal file
BIN
bluesky-connctor.zip
Normal file
Binary file not shown.
805
bluesky-connector.php
Normal file
805
bluesky-connector.php
Normal file
@ -0,0 +1,805 @@
|
||||
<?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_%'");
|
||||
}
|
70
includes/bluesky-api.php
Normal file
70
includes/bluesky-api.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
class Bluesky_API {
|
||||
private $api_url = 'https://bsky.social/xrpc'; // Replace with the actual API endpoint
|
||||
private $api_key;
|
||||
private $did;
|
||||
|
||||
public function __construct($api_key, $did) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
152
includes/bluesky-auth.php
Normal file
152
includes/bluesky-auth.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
class Bluesky_Auth {
|
||||
private $identifier;
|
||||
private $password;
|
||||
private $api_domain;
|
||||
|
||||
public function __construct($identifier, $password) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
243
includes/post-formatter.php
Normal file
243
includes/post-formatter.php
Normal file
@ -0,0 +1,243 @@
|
||||
<?php
|
||||
class Post_Formatter {
|
||||
private $api;
|
||||
private $max_length = 300;
|
||||
|
||||
public function __construct($access_token, $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')
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
2
includes/settings.php
Normal file
2
includes/settings.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Additional settings-related functions can be added here if needed
|
58
templates/post-meta-box.php
Normal file
58
templates/post-meta-box.php
Normal file
@ -0,0 +1,58 @@
|
||||
<div class="bluesky-post-status">
|
||||
<?php wp_nonce_field('bluesky_post_meta_box', 'bluesky_post_meta_box_nonce'); ?>
|
||||
|
||||
<?php if (!empty($status)) : ?>
|
||||
<p>
|
||||
<strong><?php _e('Status:', 'bluesky-connector'); ?></strong>
|
||||
<?php
|
||||
switch ($status) {
|
||||
case 'success':
|
||||
echo '<span class="bluesky-status-success">' . esc_html__('Posted', 'bluesky-connector') . '</span>';
|
||||
break;
|
||||
case 'error':
|
||||
echo '<span class="bluesky-status-error">' . esc_html__('Error', 'bluesky-connector') . '</span>';
|
||||
break;
|
||||
case 'queued':
|
||||
echo '<span class="bluesky-status-pending">' . esc_html__('Queued', 'bluesky-connector') . '</span>';
|
||||
break;
|
||||
default:
|
||||
echo '<span class="bluesky-status-unknown">' . esc_html__('Unknown', 'bluesky-connector') . '</span>';
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php if ($posted_date) : ?>
|
||||
<p>
|
||||
<strong><?php _e('Posted:', 'bluesky-connector'); ?></strong>
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($posted_date))); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($post_id) : ?>
|
||||
<p>
|
||||
<strong><?php _e('Bluesky Post ID:', 'bluesky-connector'); ?></strong>
|
||||
<code><?php echo esc_html($post_id); ?></code>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error) : ?>
|
||||
<p class="bluesky-error-message">
|
||||
<strong><?php _e('Error:', 'bluesky-connector'); ?></strong>
|
||||
<?php echo esc_html($error); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="bluesky-actions">
|
||||
<?php if ($status === 'error' || empty($post_id)) : ?>
|
||||
<button type="button" class="button bluesky-retry-post" data-post-id="<?php echo esc_attr($post->ID); ?>">
|
||||
<?php _e('Retry Post', 'bluesky-connector'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<p><?php _e('This post has not been shared to Bluesky yet.', 'bluesky-connector'); ?></p>
|
||||
<button type="button" class="button bluesky-share-post" data-post-id="<?php echo esc_attr($post->ID); ?>">
|
||||
<?php _e('Share to Bluesky', 'bluesky-connector'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
167
templates/settings-page.php
Normal file
167
templates/settings-page.php
Normal file
@ -0,0 +1,167 @@
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
|
||||
<?php if (!empty($settings['connection_status'])) : ?>
|
||||
<?php if ($settings['connection_status'] === 'connected') : ?>
|
||||
<div class="notice notice-success">
|
||||
<p><?php _e('Successfully connected to Bluesky!', 'bluesky-connector'); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="notice notice-error">
|
||||
<p><?php printf(__('Connection error: %s', 'bluesky-connector'), esc_html($settings['last_error'])); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<h2><?php _e('Connection Settings', 'bluesky-connector'); ?></h2>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('bluesky_connector_settings'); ?>
|
||||
<input type="hidden" name="action" value="update_bluesky_settings">
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bluesky_domain"><?php _e('Bluesky Domain', 'bluesky-connector'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input name="bluesky_domain"
|
||||
type="url"
|
||||
id="bluesky_domain"
|
||||
value="https://bsky.social"
|
||||
class="regular-text"
|
||||
readonly>
|
||||
<p class="description">
|
||||
<?php _e('The Bluesky API domain (fixed to https://bsky.social)', 'bluesky-connector'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bluesky_identifier"><?php _e('Bluesky Handle', 'bluesky-connector'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input name="bluesky_identifier"
|
||||
type="text"
|
||||
id="bluesky_identifier"
|
||||
value="<?php echo esc_attr($settings['identifier']); ?>"
|
||||
class="regular-text"
|
||||
placeholder="username.bsky.social"
|
||||
required>
|
||||
<p class="description">
|
||||
<?php _e('Your full Bluesky handle (e.g., username.bsky.social)', 'bluesky-connector'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bluesky_password"><?php _e('App Password', 'bluesky-connector'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input name="bluesky_password"
|
||||
type="password"
|
||||
id="bluesky_password"
|
||||
class="regular-text"
|
||||
<?php echo empty($settings['identifier']) ? 'required' : ''; ?>>
|
||||
<p class="description">
|
||||
<?php _e('Your Bluesky app password (will not be stored)', 'bluesky-connector'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(__('Save Connection Settings', 'bluesky-connector')); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($settings['connection_status']) && $settings['connection_status'] === 'connected') : ?>
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<h2><?php _e('Post Format Settings', 'bluesky-connector'); ?></h2>
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields('bluesky_connector_settings'); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bluesky_post_format"><?php _e('Post Layout', 'bluesky-connector'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="bluesky_post_format" id="bluesky_post_format">
|
||||
<option value="title-excerpt-link" <?php selected(get_option('bluesky_post_format'), 'title-excerpt-link'); ?>>
|
||||
<?php _e('Title + Excerpt + Link (No Image)', 'bluesky-connector'); ?>
|
||||
</option>
|
||||
<option value="image-title-excerpt-link" <?php selected(get_option('bluesky_post_format'), 'image-title-excerpt-link'); ?>>
|
||||
<?php _e('Image + Title + Excerpt + Link (Image will appear at top)', 'bluesky-connector'); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php _e('Note: When including images, Bluesky will always display them at the top of the post regardless of format selection.', 'bluesky-connector'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="bluesky_include_title"><?php _e('Title Options', 'bluesky-connector'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="bluesky_include_title"
|
||||
id="bluesky_include_title"
|
||||
value="1"
|
||||
<?php checked(get_option('bluesky_include_title', true)); ?>>
|
||||
<?php _e('Include post title when format includes title', 'bluesky-connector'); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php _e('When enabled, the post title will be included at the beginning of the post text.', 'bluesky-connector'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button(__('Save Format Settings', 'bluesky-connector')); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<h2><?php _e('Queue Management', 'bluesky-connector'); ?></h2>
|
||||
<?php
|
||||
$queue = get_option('bluesky_post_queue', array());
|
||||
$queue_count = count($queue);
|
||||
?>
|
||||
<p>
|
||||
<?php printf(
|
||||
_n(
|
||||
'There is %s post in the queue.',
|
||||
'There are %s posts in the queue.',
|
||||
$queue_count,
|
||||
'bluesky-connector'
|
||||
),
|
||||
number_format_i18n($queue_count)
|
||||
); ?>
|
||||
</p>
|
||||
|
||||
<?php if ($queue_count > 0) : ?>
|
||||
<form method="post" style="margin-top: 10px;">
|
||||
<?php wp_nonce_field('bluesky_process_queue'); ?>
|
||||
<input type="submit" name="process_queue_now" class="button button-primary"
|
||||
value="<?php esc_attr_e('Process Queue Now', 'bluesky-connector'); ?>">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (WP_DEBUG) : ?>
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<h2><?php _e('Connection Status', 'bluesky-connector'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('DID', 'bluesky-connector'); ?></th>
|
||||
<td><code><?php echo esc_html(get_option('bluesky_did')); ?></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Last Token Refresh', 'bluesky-connector'); ?></th>
|
||||
<td><?php echo esc_html(get_option('bluesky_token_created') ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), get_option('bluesky_token_created')) : __('Never', 'bluesky-connector')); ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user