Source: wp-to-twitter.php

<?php
/**
 * XPoster
 *
 * @package     XPoster
 * @author      Joe Dolson
 * @copyright   2008-2024 Joe Dolson
 * @license     GPL-2.0+
 *
 * @wordpress-plugin
 * Plugin Name: XPoster - Share to Bluesky and Mastodon
 * Plugin URI:  https://www.joedolson.com/wp-to-twitter/
 * Description: Posts a status update when you update your WordPress blog or post a link, using your URL shortener. Many options to customize and promote your statuses.
 * Author:      Joe Dolson
 * Author URI:  https://www.joedolson.com
 * Text Domain: wp-to-twitter
 * License:     GPL-2.0+
 * License URI: http://www.gnu.org/license/gpl-2.0.txt
 * Domain Path: lang
 * Version:     4.3.1
 */

/*
	Copyright 2008-2024  Joe Dolson (email : joe@joedolson.com)

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

use WpToTwitter_Vendor\GuzzleHttp\Exception\RequestException;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$wpt_debug = get_option( 'wpt_debug_tweets', false );
define( 'WPT_DEBUG', $wpt_debug );
define( 'WPT_DEBUG_BY_EMAIL', false ); // Email debugging no longer default as of 3.3.0.
define( 'WPT_DEBUG_ADDRESS', get_option( 'admin_email' ) );
define( 'WPT_FROM', 'From: \"' . get_option( 'blogname' ) . '\" <' . get_option( 'admin_email' ) . '>' );

// If current environment tests as staging, enable staging mode.
if ( function_exists( 'wp_get_environment_type' ) ) {
	if ( 'staging' === wp_get_environment_type() && ! defined( 'WPT_STAGING_MODE' ) ) {
		define( 'WPT_STAGING_MODE', true );
	}
}

require_once plugin_dir_path( __FILE__ ) . 'vendor_prefixed/vendor/scoper-autoload.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/metabox.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/deprecated.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-functions.php';
// Service post handlers.
require_once plugin_dir_path( __FILE__ ) . 'wpt-post-to-twitter.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-post-to-mastodon.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-post-to-bluesky.php';
// URL Shortening.
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-shorteners.php';
// Service settings.
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-oauth.php';
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-mastodon.php';
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-bluesky.php';
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-manager.php';
// Template generation.
require_once plugin_dir_path( __FILE__ ) . 'wpt-truncate.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-rate-limiting.php';

global $wpt_version;
$wpt_version = '4.3.0';

/**
 * Check for OAuth configuration
 *
 * @param mixed int/boolean $auth Which account to check.
 *
 * @return boolean Whether authorized.
 */
function wpt_check_oauth( $auth = false ) {
	if ( ! function_exists( 'wtt_oauth_test' ) ) {
		$oauth = false;
	} else {
		$oauth = wtt_oauth_test( $auth );
	}

	return $oauth;
}

/**
 * Check whether version requires activation.
 */
function wpt_check_version() {
	global $wpt_version;
	$prev_version = ( '' !== get_option( 'wp_to_twitter_version', '' ) ) ? get_option( 'wp_to_twitter_version' ) : '1.0.0';
	if ( version_compare( $prev_version, $wpt_version, '<' ) ) {
		xposter_activate();
	}
}

/**
 * Activate XPoster.
 */
function xposter_activate() {
	// If this has never run before, do the initial setup.
	$new_install = ( '1' === get_option( 'wpt_twitter_setup' ) || '1' === get_option( 'twitterInitialised' ) ) ? false : true;
	if ( $new_install ) {
		$initial_settings = array(
			'post' => array(
				'post-published-update' => '1',
				'post-published-text'   => 'New post: #title# #url#',
				'post-edited-update'    => '0',
				'post-edited-text'      => 'Post Edited: #title# #url#',
			),
			'page' => array(
				'post-published-update' => '0',
				'post-published-text'   => 'New page: #title# #url#',
				'post-edited-update'    => '0',
				'post-edited-text'      => 'Page edited: #title# #url#',
			),
		);
		update_option( 'wpt_post_types', $initial_settings );
		update_option( 'jd_twit_blogroll', '1' );
		update_option( 'newlink-published-text', 'New link: #title# #url#' );
		update_option( 'jd_shortener', '3' );
		update_option( 'jd_strip_nonan', '0' );
		update_option( 'jd_max_tags', 4 );
		update_option( 'jd_max_characters', 20 );
		$administrator = get_role( 'administrator' );
		if ( is_object( $administrator ) ) {
			// wpt_twitter_oauth is the general permission for editing user accounts.
			$administrator->add_cap( 'wpt_twitter_oauth' );
			$administrator->add_cap( 'wpt_twitter_custom' );
			$administrator->add_cap( 'wpt_twitter_switch' );
			$administrator->add_cap( 'wpt_can_tweet' );
			$administrator->add_cap( 'wpt_tweet_now' );
		}
		$editor = get_role( 'editor' );
		if ( is_object( $editor ) ) {
			$editor->add_cap( 'wpt_can_tweet' );
		}
		$author = get_role( 'author' );
		if ( is_object( $author ) ) {
			$author->add_cap( 'wpt_can_tweet' );
		}
		$contributor = get_role( 'contributor' );
		if ( is_object( $contributor ) ) {
			$contributor->add_cap( 'wpt_can_tweet' );
		}

		update_option( 'jd_post_excerpt', 60 );
		// Use Google Analytics.
		update_option( 'twitter-analytics-campaign', 'twitter' );
		update_option( 'use-twitter-analytics', '0' );
		update_option( 'jd_dynamic_analytics', '0' );
		update_option( 'no-analytics', 1 );
		update_option( 'use_dynamic_analytics', 'category' );
		// Use custom external URLs to point elsewhere.
		update_option( 'jd_twit_custom_url', 'external_link' );
		// Error checking.
		update_option( 'wp_url_failure', '0' );
		// Default publishing options.
		update_option( 'jd_tweet_default', '0' );
		update_option( 'jd_tweet_default_edit', '0' );
		update_option( 'wpt_inline_edits', '0' );
		// Note that default options are set.
		update_option( 'wpt_twitter_setup', '1' );
		update_option( 'jd_keyword_format', '0' );
	}

	global $wpt_version;
	$prev_version  = get_option( 'wp_to_twitter_version' );
	$administrator = get_role( 'administrator' );
	$upgrade       = version_compare( $prev_version, '2.9.0', '<' );
	if ( $upgrade ) {
		$administrator->add_cap( 'wpt_tweet_now' );
	}
	$upgrade = version_compare( $prev_version, '3.4.4', '<' );
	if ( $upgrade ) {
		delete_option( 'bitlyapi' );
		delete_option( 'bitlylogin' );
	}

	update_option( 'wp_to_twitter_version', $wpt_version );
}

/**
 * Function checks for an alternate URL to be updated. Contribution by Bill Berry.
 *
 * @param int $post_ID Post ID.
 *
 * @return Link to use for this URL.
 */
function wpt_link( $post_ID ) {
	$ex_link       = false;
	$external_link = get_option( 'jd_twit_custom_url', '' );
	$permalink     = get_permalink( $post_ID );
	if ( '' !== $external_link ) {
		$ex_link = get_post_meta( $post_ID, $external_link, true );
	}

	return ( $ex_link ) ? $ex_link : $permalink;
}

/**
 * Save error messages for status updates.
 *
 * @param int    $id Post ID.
 * @param int    $auth Current author.
 * @param string $twit Update text.
 * @param string $error Error string from service.
 * @param int    $http_code Http code from service.
 * @param string $ts Current timestamp.
 */
function wpt_save_error( $id, $auth, $twit, $error, $http_code, $ts ) {
	$http_code = (int) $http_code;
	if ( 200 !== $http_code ) {
		add_post_meta(
			$id,
			'_wpt_failed',
			array(
				'author'    => $auth,
				'sentence'  => $twit,
				'error'     => $error,
				'code'      => $http_code,
				'timestamp' => $ts,
			)
		);
	} else {
		if ( '1' === get_option( 'wpt_rate_limiting' ) ) {
			wpt_log_success( $auth, $ts, $id );
		}
	}
}

/**
 * Save a record of a successful status update.
 *
 * @param int    $id Post ID.
 * @param string $twit Status update text.
 * @param int    $http_code HTTP Code returned by query.
 */
function wpt_save_success( $id, $twit, $http_code ) {
	if ( 200 === $http_code ) {
		$jwt = get_post_meta( $id, '_jd_wp_twitter', true );
		if ( ! is_array( $jwt ) ) {
			$jwt = array();
		}
		$jwt[] = urldecode( $twit );
		// Why do I pass this into the POST array? Is this something from a past version? Todo.
		if ( empty( $_POST ) ) {
			$_POST = array();
		}
		$_POST['_jd_wp_twitter'] = $jwt;
		update_post_meta( $id, '_jd_wp_twitter', $jwt );
	}
}

/**
 * Checks whether XPoster has sent a tweet on this post to this author within the last 30 seconds and blocks duplicates.
 *
 * @param int $id Post ID.
 * @param int $auth Author.
 *
 * @uses filter wpt_recent_tweet_threshold
 * @return boolean true to send status update, false to block.
 */
function wpt_check_recent_tweet( $id, $auth ) {
	if ( ! $id ) {
		return false;
	} else {
		if ( false === $auth ) {
			$transient = get_transient( "_wpt_most_recent_tweet_$id" );
		} else {
			$transient = get_transient( '_wpt_' . $auth . "_most_recent_tweet_$id" );
		}
		if ( $transient ) {
			return true;
		} else {
			/**
			 * Modify the expiration window for recent status updates.
			 * This value does flood control, to prevent a runaway process from sending multiple status updates. Default `30` seconds.
			 *
			 * @hook wpt_recent_tweet_threshold
			 * @param {int} $expire Integer representing seconds. How long the transient will exist.
			 *
			 * @return {int}
			 */
			$expire = apply_filters( 'wpt_recent_tweet_threshold', 30 );
			// if expiration is 0, don't set the transient. We don't want permanent transients.
			if ( 0 !== $expire ) {
				wpt_mail( 'Update transient set', "$expire / $auth / $id", $id );
				if ( false === $auth ) {
					set_transient( "_wpt_most_recent_tweet_$id", true, $expire );
				} else {
					set_transient( '_wpt_' . $auth . "_most_recent_tweet_$id", true, $expire );
				}
			}
		}
	}

	return false;
}


/**
 * Performs the API post to target services. Alias for wpt_post_to_twitter.
 *
 * @param string  $text Text of post to be sent to service.
 * @param int     $auth Author ID.
 * @param int     $id Post ID.
 * @param boolean $media Whether to upload media attached to the post specified in $id.
 *
 * @return boolean|array False if blocked, array of statuses if attempted.
 */
function wpt_post_to_service( $text, $auth = false, $id = false, $media = false ) {
	$return = wpt_post_to_twitter( $text, $auth, $id, $media );

	return $return;
}

/**
 * Performs the API post to target services.
 *
 * @param string  $twit Text of post to be sent to service.
 * @param int     $auth Author ID.
 * @param int     $id Post ID.
 * @param boolean $media Whether to upload media attached to the post specified in $id.
 *
 * @return boolean|array False if blocked, array of statuses if attempted.
 */
function wpt_post_to_twitter( $twit, $auth = false, $id = false, $media = false ) {
	// If an ID is set but the post is not currently present or published, ignore.
	$return = array();
	if ( $id ) {
		$status = get_post_status( $id );
		if ( ! $status || 'publish' !== $status ) {
			$error = __( 'This post is no longer published or has been deleted', 'wp-to-twitter' );
			wpt_save_error( $id, $auth, $twit, $error, '404', time() );
			wpt_set_log( 'wpt_status_message', $id, $error, '404' );

			return false;
		}
	}
	$error = false;
	if ( '1' === get_option( 'wpt_rate_limiting' ) ) {
		// check whether this post needs to be rate limited.
		$continue = wpt_test_rate_limit( $id, $auth );
		if ( ! $continue ) {
			wpt_mail( 'This post was blocked by XPoster rate limiting.', 'Post ID: ' . $id . '; Account: ' . $auth );

			return false;
		}
	}

	$recent = wpt_check_recent_tweet( $id, $auth );
	if ( $recent ) {
		wpt_mail( 'This post was just sent, and this is a duplicate.', 'Post ID: ' . $id . '; Account: ' . $auth );

		return false;
	}

	$check_twitter  = wpt_check_oauth( $auth );
	$check_mastodon = wpt_mastodon_connection( $auth );
	$check_bluesky  = wpt_bluesky_connection( $auth );
	if ( ! $check_twitter && ! $check_mastodon && ! $check_bluesky ) {
		$error = __( 'This account is not authorized to post to any services.', 'wp-to-twitter' );
		wpt_save_error( $id, $auth, $twit, $error, '401', time() );
		wpt_set_log( 'wpt_status_message', $id, $error, '401' );
		if ( ! $check_twitter ) {
			wpt_mail( 'Account not authorized with X.com API.', 'Post ID: ' . $id );
		}
		if ( ! $check_mastodon ) {
			wpt_mail( 'Account not authorized with Mastodon.', 'Post ID: ' . $id );
		}
		if ( ! $check_bluesky ) {
			wpt_mail( 'Account not authorized with Bluesky.', 'Post ID: ' . $id );
		}

		return false;
	} // exit silently if not authorized.

	$check = ( ! $auth ) ? get_option( 'jd_last_tweet', '' ) : get_user_meta( $auth, 'wpt_last_tweet', true ); // get user's last tweet.
	// prevent duplicate status updates. Checks whether this text has already been sent.
	if ( $check === $twit && '' !== $twit ) {
		wpt_mail( 'Matched: status update identical', "This Update: $twit; Check Update: $check; $auth, $id, $media", $id ); // DEBUG.
		$error = __( 'This status update is identical to another update recently sent to this account.', 'wp-to-twitter' ) . ' ' . __( 'All status updates are expected to be unique.', 'wp-to-twitter' );
		wpt_save_error( $id, $auth, $twit, $error, '403-1', time() );
		wpt_set_log( 'wpt_status_message', $id, $error, '403' );

		return false;
	} elseif ( '' === $twit || ! $twit ) {
		wpt_mail( 'Status update check: empty sentence', "$twit, $auth, $id, $media", $id ); // DEBUG.
		$error = __( 'This status update was blank and could not be sent to the API.', 'wp-to-twitter' );
		wpt_save_error( $id, $auth, $twit, $error, '403-2', time() );
		wpt_set_log( 'wpt_status_message', $id, $error, '403' );

		return false;
	} else {
		// must be designated as media and have a valid attachment.
		$attachment = ( $media ) ? wpt_post_attachment( $id ) : false;
		if ( $attachment ) {
			wpt_mail( 'Post has upload', "$auth, $attachment", $id );
			$meta = wp_get_attachment_metadata( $attachment );
			if ( ! isset( $meta['width'], $meta['height'] ) ) {
				wpt_mail( "Image Data Does not Exist for #$attachment", print_r( $meta, 1 ), $id );
				$attachment = false;
			}
		}

		$status = array(
			'text' => $twit,
		);

		$connection = false;
		if ( wtt_oauth_test( $auth ) ) {
			$connection = wpt_oauth_connection( $auth );
			$status     = wpt_upload_twitter_media( $connection, $auth, $attachment, $status, $id );
			$response   = wpt_send_post_to_twitter( $connection, $auth, $id, $status );
			wpt_post_submit_handler( $connection, $response, $id, $auth, $twit );
			$return['xcom'] = $response;
		}
		if ( wpt_mastodon_connection( $auth ) ) {
			$connection = wpt_mastodon_connection( $auth );
			$status     = wpt_upload_mastodon_media( $connection, $auth, $attachment, $status, $id );
			$response   = wpt_send_post_to_mastodon( $connection, $auth, $id, $status );
			wpt_post_submit_handler( $connection, $response, $id, $auth, $twit );
			$return['mastodon'] = $response;
		}
		if ( wpt_bluesky_connection( $auth ) ) {
			$connection = wpt_bluesky_connection( $auth );
			$image      = wpt_upload_bluesky_media( $connection, $auth, $attachment, $status, $id );
			$response   = wpt_send_post_to_bluesky( $connection, $auth, $id, $status, $image );
			wpt_post_submit_handler( $connection, $response, $id, $auth, $twit );
			$return['bluesky'] = $response;
		}
		wpt_mail( 'Share Connection Status', "$twit, $auth, $id, $media, " . print_r( $response, 1 ), $id );
		if ( ! empty( $return ) ) {

			return $return;
		} else {
			wpt_set_log( 'wpt_status_message', $id, __( 'No API connection found.', 'wp-to-twitter' ), '404' );

			return false;
		}
	}
}

/**
 * Handle post-sending responses for APIs.
 *
 * @param object   $connection API connection.
 * @param array    $response Array of response data from API.
 * @param int      $id Post ID.
 * @param int|bool $auth Author context.
 * @param string   $twit Posted status text.
 */
function wpt_post_submit_handler( $connection, $response, $id, $auth, $twit ) {
	$return      = $response['return'];
	$http_code   = $response['http'];
	$notice      = $response['notice'];
	$service     = isset( $response['service'] ) ? $response['service'] : false;
	$tweet_id    = ( 'x' === $service ) ? $response['status_id'] : false;
	$mastodon_id = ( 'mastodon' === $service ) ? $response['status_id'] : false;
	$bluesky_id  = ( 'bluesky' === $service ) ? $response['status_id'] : false;
	wpt_mail( "Status Update Response: $http_code / $service", $notice, $id ); // DEBUG.
	// only save last status if successful.
	if ( 200 === $http_code ) {
		if ( ! $auth ) {
			update_option( 'jd_last_tweet', $twit );
		} else {
			update_user_meta( $auth, 'wpt_last_tweet', $twit );
		}
	}
	wpt_save_error( $id, $auth, $twit, $notice, $http_code, time() );
	wpt_save_success( $id, $twit, $http_code );
	if ( ! $return ) {
		/**
		 * Executes an action after posting a status fails.
		 *
		 * @hook wpt_tweet_failed
		 *
		 * @since 3.6.0
		 *
		 * @param {object} $connection The current OAuth connection.
		 * @param {int}    $id Post ID for status update.
		 * @param {string} $error Error message returned.
		 */
		do_action( 'wpt_tweet_failed', $connection, $id, $notice );
		wpt_set_log( 'wpt_status_message', $id, $notice, $http_code );
	} else {
		/**
		 * Executes an action after a status is posted successfully.
		 *
		 * @hook wpt_tweet_posted
		 *
		 * @param {object} $connection The current OAuth connection.
		 * @param {int}    $id Post ID for status update.
		 */
		do_action( 'wpt_tweet_posted', $connection, $id );
		// Log the Status ID of the first status update on this post.
		$has_tweet_id = get_post_meta( $id, '_wpt_tweet_id', true );
		if ( ! $has_tweet_id && $tweet_id ) {
			update_post_meta( $id, '_wpt_tweet_id', $tweet_id );
		}
		// Log the Status ID of the first Mastodon update on this post.
		$has_mastodon_id = get_post_meta( $id, '_wpt_status_id', true );
		if ( ! $has_mastodon_id && $mastodon_id ) {
			update_post_meta( $id, '_wpt_status_id', $mastodon_id );
		}
		// Log the Status ID of the first Bluesky update on this post.
		$has_bluesky_id = get_post_meta( $id, '_wpt_bluesky_id', true );
		if ( ! $has_bluesky_id && $bluesky_id ) {
			update_post_meta( $id, '_wpt_bluesky_id', $bluesky_id );
		}
		wpt_set_log( 'wpt_status_message', $id, $notice );
	}
}

/**
 * Get text error message from HTTP code.
 *
 * @param int      $http_code HTTP returned.
 * @param string   $notice Any already generated notification message.
 * @param int|bool $auth Current authentication context.
 *
 * @return array
 */
function wpt_get_response_message( $http_code, $notice, $auth ) {
	$return = false;
	switch ( $http_code ) {
		case '000':
			$error = '';
			break;
		case 100:
			$error = __( '100 Continue: X received the header of your submission, but your server did not follow through by sending the body of the data.', 'wp-to-twitter' );
			break;
		case 200:
			$return = true;
			$error  = __( '200 OK: Success!', 'wp-to-twitter' );
			update_option( 'wpt_authentication_missing', false );
			break;
		case 304:
			$error = __( '304 Not Modified: There was no new data to return', 'wp-to-twitter' );
			break;
		case 400:
			// Translators: Error description from X.com.
			$error = sprintf( __( '400: %s', 'wp-to-twitter' ), $notice );
			break;
		case 401:
			// Translators: Error description from X.com.
			$error = sprintf( __( '401: %s', 'wp-to-twitter' ), $notice );
			update_option( 'wpt_authentication_missing', "$auth" );
			break;
		case 403:
			// Translators: Error description from X.com.
			$error = sprintf( __( '403: %s', 'wp-to-twitter' ), $notice );
			break;
		case 404:
			// Translators: Error description from X.com.
			$error = sprintf( __( '404: %s', 'wp-to-twitter' ), $notice );
			break;
		case 406:
			// Translators: Error description from X.com.
			$error = sprintf( __( '406: %s', 'wp-to-twitter' ), $notice );
			break;
		case 422:
			// Translators: Error description from X.com.
			$error = sprintf( __( '422: %s', 'wp-to-twitter' ), $notice );
			break;
		case 429:
			// Translators: Error description from X.com.
			$error = sprintf( __( '429: %s', 'wp-to-twitter' ), $notice );
			break;
		case 500:
			$error = __( '500 Internal Server Error: Something is broken at X.com.', 'wp-to-twitter' );
			break;
		case 502:
			$error = __( '502 Bad Gateway: X.com is down or being upgraded.', 'wp-to-twitter' );
			break;
		case 503:
			$error = __( '503 Service Unavailable: The X.com servers are up, but overloaded with requests - Please try again later.', 'wp-to-twitter' );
			break;
		case 504:
			$error = __( "504 Gateway Timeout: The X.com servers are up, but the request couldn't be serviced due to some failure within our stack. Try again later.", 'wp-to-twitter' );
			break;
		default:
			// Translators: http code.
			$error = sprintf( __( '<strong>Code %s</strong>: X.com did not return a recognized response code.', 'wp-to-twitter' ), $http_code );
			break;
	}
	return array(
		'error'  => $error,
		'return' => $return,
	);
}

/**
 * Get image binary for passing to API.
 *
 * @param int    $attachment Attachment ID.
 * @param string $service Which service needs the binary.
 *
 * @return string|object;
 */
function wpt_image_binary( $attachment, $service = 'twitter' ) {
	$image_sizes = get_intermediate_image_sizes();
	if ( in_array( 'large', $image_sizes, true ) ) {
		$size = 'large';
	} else {
		$size = array_pop( $image_sizes );
	}
	/**
	 * Filter the uploaded image size.
	 *
	 * @hook wpt_upload_image_size
	 *
	 * @param string $size Name of size targeted for upload. Default 'large' if exists.
	 *
	 * @return string
	 */
	$size   = apply_filters( 'wpt_upload_image_size', $size );
	$parent = get_post_ancestors( $attachment );
	$parent = ( is_array( $parent ) && isset( $parent[0] ) ) ? $parent[0] : false;
	if ( 'mastodon' === $service ) {
		$path      = wpt_attachment_path( $attachment, $size );
		$mime      = wp_get_image_mime( $path );
		$name      = basename( $path );
		$file      = curl_file_create( $path, $mime, $name );
		$transport = 'curl';
		wpt_mail( 'XPoster: media binary fetched', 'Path: ' . $path . 'Transport: ' . $transport . PHP_EOL . $attachment, $parent );
		if ( ! $file ) {
			return false;
		}

		return $file;

	} elseif ( 'bluesky' === $service ) {
		$path = wpt_attachment_path( $attachment, $size );
		global $wp_filesystem;
		require_once ABSPATH . '/wp-admin/includes/file.php';
		WP_Filesystem();
		$file = $wp_filesystem->get_contents( $path );

		return $file;
	} else {
		$upload    = wp_get_attachment_image_src( $attachment, $size );
		$image_url = $upload[0];
		$remote    = wp_remote_get( $image_url );
		if ( is_wp_error( $remote ) ) {
			$transport = 'curl';
			$binary    = wp_get_curl( $image_url );
		} else {
			$transport = 'wp_http';
			$binary    = wp_remote_retrieve_body( $remote );
		}
		wpt_mail( 'XPoster: media binary fetched', 'Url: ' . $image_url . 'Transport: ' . $transport . print_r( $remote, 1 ), $parent );
		if ( ! $binary ) {
			return false;
		}
		// TODO: should this be encoded or not?
		return base64_encode( $binary );
	}
}

/**
 * Fetch an attachment's file path. Recurses to fetch full sized path if an invalid size is passed.
 *
 * @param int    $attachment_id Attachment ID.
 * @param string $size Requested size.
 *
 * @return string|false
 */
function wpt_attachment_path( $attachment_id, $size = '' ) {
	$file = get_attached_file( $attachment_id, true );
	if ( empty( $size ) || 'full' === $size ) {
		// for the original size get_attached_file is fine.
		return realpath( $file );
	}
	if ( ! wp_attachment_is_image( $attachment_id ) ) {
		return false; // the id is not referring to a media.
	}
	$info = image_get_intermediate_size( $attachment_id, $size );
	if ( ! is_array( $info ) || ! isset( $info['file'] ) ) {
		// If this is invalid due to an invalid size, recurse to fetch full size.
		if ( '' !== $size ) {
			$path = wpt_attachment_path( $attachment_id );

			return $path;
		}
		return false; // probably a bad size argument.
	}

	return realpath( str_replace( wp_basename( $file ), $info['file'], $file ) );
}

/**
 * For servers without PEAR normalize installed, approximates normalization. With normalizer, executes normalization on string.
 *
 * @param string $text Text to normalize.
 *
 * @return string Normalized text.
 */
function wpt_normalize( $text ) {
	if ( version_compare( PHP_VERSION, '5.0.0', '>=' ) && function_exists( 'normalizer_normalize' ) ) {
		if ( normalizer_is_normalized( $text ) ) {
			return $text;
		}

		return normalizer_normalize( $text );
	} else {
		$normalizer = new WPT_Normalizer();
		if ( $normalizer->is_normalized( $text ) ) {
			return $text;
		}

		return $normalizer->normalize( $text );
	}
}

/**
 * Test URL to see if is pointing to https location.
 *
 * @param string $url URL to check for https.
 *
 * @return boolean
 */
function wpt_is_ssl( $url ) {
	if ( stripos( $url, 'https' ) ) {
		return true;
	} else {
		return false;
	}
}

/**
 * Builds array of post info for use in status update functions.
 *
 * @param integer $post_ID Post ID.
 *
 * @return array Post data used in status update functions.
 */
function wpt_post_info( $post_ID ) {
	$encoding     = get_option( 'blog_charset', 'UTF-8' );
	$post         = get_post( $post_ID );
	$category_ids = array();
	$values       = array();
	$values['id'] = $post_ID;
	// get post author.
	$values['postinfo']      = $post;
	$values['postContent']   = $post->post_content;
	$values['authId']        = $post->post_author;
	$postdate                = $post->post_date;
	$dateformat              = ( '' === get_option( 'jd_date_format', '' ) ) ? get_option( 'date_format' ) : get_option( 'jd_date_format' );
	$thisdate                = mysql2date( $dateformat, $postdate );
	$altdate                 = mysql2date( 'Y-m-d H:i:s', $postdate );
	$values['_postDate']     = $altdate;
	$values['postDate']      = $thisdate;
	$moddate                 = $post->post_modified;
	$values['_postModified'] = mysql2date( 'Y-m-d H:i:s', $moddate );
	$values['postModified']  = mysql2date( $dateformat, $moddate );
	// get first category.
	$category   = '';
	$cat_desc   = '';
	$categories = get_the_category( $post_ID );
	$cats       = array();
	$cat_descs  = array();
	if ( is_array( $categories ) ) {
		if ( count( $categories ) > 0 ) {
			$category = $categories[0]->cat_name;
			$cat_desc = $categories[0]->description;
		}
		foreach ( $categories as $cat ) {
			$category_ids[] = $cat->term_id;
			$cats[]         = $cat->cat_name;
			$cat_descs[]    = $cat->description;
		}
		/**
		 * Filter the space separated list of category names in #cats#.
		 *
		 * @hook wpt_twitter_category_names
		 * @param {array} $cats Array of category names attached to this status update.
		 *
		 * @return {array}
		 */
		$cat_names = implode( ' ', apply_filters( 'wpt_twitter_category_names', $cats ) );
		/**
		 * Filter the space separated list of category descriptions in #cat_descs#.
		 *
		 * @hook wpt_twitter_category_descs
		 * @param {array} $cats Array of category descriptions attached to this status update.
		 *
		 * @return {array}
		 */
		$cat_descs = implode( ' ', apply_filters( 'wpt_twitter_category_descs', $cat_descs ) );
	} else {
		$category     = '';
		$cat_desc     = '';
		$category_ids = array();
	}
	$values['cats']        = $cat_names;
	$values['cat_descs']   = $cat_descs;
	$values['categoryIds'] = $category_ids;
	$values['category']    = ( $category ) ? html_entity_decode( $category, ENT_COMPAT, $encoding ) : '';
	$values['cat_desc']    = ( $cat_desc ) ? html_entity_decode( $cat_desc, ENT_COMPAT, $encoding ) : '';
	$excerpt_length        = get_option( 'jd_post_excerpt' );
	$post_excerpt          = ( '' === trim( $post->post_excerpt ) ) ? mb_substr( strip_tags( strip_shortcodes( $post->post_content ) ), 0, $excerpt_length ) : mb_substr( strip_tags( strip_shortcodes( $post->post_excerpt ) ), 0, $excerpt_length );
	$values['postExcerpt'] = ( $post_excerpt ) ? html_entity_decode( $post_excerpt, ENT_COMPAT, $encoding ) : '';
	$thisposttitle         = $post->post_title;
	if ( '' === $thisposttitle && isset( $_POST['title'] ) ) {
		$thisposttitle = wp_kses_post( $_POST['title'] );
	}
	$thisposttitle = strip_tags( apply_filters( 'the_title', stripcslashes( $thisposttitle ), $post_ID ) );
	// These are common sequences that may not be fixed by html_entity_decode due to double encoding.
	$search               = array( '&apos;', '&#039;', '&quot;', '&#034;', '&amp;', '&#038;' );
	$replace              = array( "'", "'", '"', '"', '&', '&' );
	$thisposttitle        = str_replace( $search, $replace, $thisposttitle );
	$values['postTitle']  = html_entity_decode( $thisposttitle, ENT_QUOTES, $encoding );
	$values['postLink']   = wpt_link( $post_ID );
	$values['blogTitle']  = get_bloginfo( 'name' );
	$values['shortUrl']   = wpt_short_url( $post_ID );
	$values['postStatus'] = $post->post_status;
	$values['postType']   = $post->post_type;
	/**
	 * Filters post array to insert custom data that can be used in status update process.
	 *
	 * @param array   $values Existing values.
	 * @param integer $post_ID Post ID.
	 * @return array  $values
	 */
	$values = apply_filters( 'wpt_post_info', $values, $post_ID );

	return $values;
}

/**
 * Retrieve stored short URL.
 *
 * @param int $post_id Post ID.
 *
 * @return string|bool False if no stored URL.
 */
function wpt_short_url( $post_id ) {
	global $post_ID;
	if ( ! $post_id ) {
		$post_id = $post_ID;
	}
	$use_urls = ( get_option( 'wpt_use_stored_urls' ) === 'false' ) ? false : true;
	$short    = ( $use_urls ) ? get_post_meta( $post_id, '_wpt_short_url', true ) : false;
	$short    = ( '' === $short ) ? false : $short;

	return $short;
}

/**
 * Identify whether a post should be uploading media. Test settings and verify whether post has images that can be uploaded.
 *
 * @param int   $post_ID Post ID.
 * @param array $post_info Array of post data.
 *
 * @return boolean
 */
function wpt_post_with_media( $post_ID, $post_info = array() ) {
	$return = false;
	if ( ! function_exists( 'wpt_pro_exists' ) ) {
		return $return;
	}
	if ( isset( $post_info['wpt_image'] ) && 1 === (int) $post_info['wpt_image'] ) {
		// Post settings win over filters.
		return $return;
	}
	if ( ! get_option( 'wpt_media' ) ) {
		// Don't return immediately, this needs to be overrideable for posts.
		$return = false;
	} else {
		if ( has_post_thumbnail( $post_ID ) || wpt_post_attachment( $post_ID ) ) {
			$return = true;
		}
	}
	/**
	 * Filter whether this post should upload media.
	 *
	 * @hook wpt_upload_media
	 * @param {bool} $upload True to allow this post to upload media.
	 * @param {int}  $post_ID Post ID.
	 *
	 * @return {bool}
	 */
	return apply_filters( 'wpt_upload_media', $return, $post_ID );
}

/**
 * This function is no longer in use, but the filter within it is.
 *
 * @param string $post_type Type of post.
 * @param array  $post_info Post info.
 * @param int    $post_ID Post ID.
 *
 * @return bool False to block this from posting.
 */
function wpt_category_limit( $post_type, $post_info, $post_ID ) {
	$continue = true;
	$args     = array(
		'type' => $post_type,
		'info' => $post_info,
		'id'   => $post_ID,
	);

	return apply_filters( 'wpt_filter_terms', $continue, $args );
}

/**
 * Set up a status update to be sent.
 *
 * @param int     $post_ID Post ID.
 * @param string  $type Publishing context: instant, future, xmlrpc.
 * @param object  $post Post object.
 * @param boolean $updated True if updated, false if inserted.
 * @param object  $post_before The post prior to this update, or null for new posts.
 *
 * @return int $post_ID
 */
function wpt_post_update( $post_ID, $type = 'instant', $post = null, $updated = null, $post_before = null ) {
	if ( wp_is_post_autosave( $post_ID ) || wp_is_post_revision( $post_ID ) ) {
		return $post_ID;
	}
	wpt_check_version();
	$post_this      = get_post_meta( $post_ID, '_wpt_post_this', true );
	$newpost        = false;
	$oldpost        = false;
	$is_inline_edit = false;
	$sentence       = '';
	$template       = '';
	$nptext         = '';
	if ( '1' !== get_option( 'wpt_inline_edits' ) ) {
		if ( isset( $_POST['_inline_edit'] ) || isset( $_REQUEST['bulk_edit'] ) ) {
			return false;
		}
	} else {
		if ( isset( $_POST['_inline_edit'] ) || isset( $_REQUEST['bulk_edit'] ) ) {
			$is_inline_edit = true;
		}
	}
	if ( '0' === get_option( 'jd_tweet_default' ) ) {
		// If post this value is not set or equals 'yes'.
		$default      = ( 'no' !== $post_this ) ? true : false;
		$text_default = 'no';
	} else {
		// If post this is set and is equal to yes.
		$default      = ( 'yes' === $post_this ) ? true : false;
		$text_default = 'yes';
	}
	wpt_mail( '1: Status Update should send: ' . $post_this, "Default: $text_default; Publication method: $type", $post_ID ); // DEBUG.
	if ( $default ) { // default switch: depend on default settings.
		$post_info = wpt_post_info( $post_ID );
		$media     = wpt_post_with_media( $post_ID, $post_info );
		if ( function_exists( 'wpt_pro_exists' ) && true === wpt_pro_exists() ) {
			$auth = ( 'false' === get_option( 'wpt_cotweet_lock' ) || ! get_option( 'wpt_cotweet_lock' ) ) ? $post_info['authId'] : get_option( 'wpt_cotweet_lock' );
		} else {
			$auth = $post_info['authId'];
		}
		$debug_post_info = $post_info;
		unset( $debug_post_info['post_content'] );
		unset( $debug_post_info['postContent'] );
		wpt_mail( '2: XPoster Post Info (post content omitted)', print_r( $debug_post_info, 1 ), $post_ID ); // DEBUG.
		/**
		 * Apply filters against this post to determine whether it should be allowed to be sent.
		 *
		 * @hook wpt_should_block_status
		 *
		 * @param {bool}  $filter Always false by default.
		 * @param {array} $post_info Array of post data.
		 *
		 * @return {bool} True to block post.
		 */
		$filter = apply_filters( 'wpt_should_block_status', false, $post_info );
		if ( true === $filter ) {
			wpt_mail( '3: Post blocked by XPoster Pro custom filters', 'No additional data available', $post_ID );

			return false;
		}
		/**
		 * Return true to ignore this post based on POST data. Default false.
		 *
		 * @hook wpt_filter_post_data
		 * @param {bool} $filter True if this post should not have a status update sent.
		 * @param {array} $post POST global.
		 *
		 * @return {bool}
		 */
		$filter = apply_filters( 'wpt_filter_post_data', false, $_POST );
		if ( $filter ) {
			return false;
		}
		$post_type = $post_info['postType'];
		if ( 'future' === $type || 'future' === get_post_meta( $post_ID, 'wpt_publishing', true ) ) {
			$new = 1; // if this is a future action, then it should be published regardless of relationship.
			wpt_mail( '4a: Post is a scheduled post', 'See Post Info data', $post_ID );
			delete_post_meta( $post_ID, 'wpt_publishing' );
		} else {
			// if the post modified date and the post date are the same, this is new.
			// true if first date before or equal to last date.
			$new = wpt_post_is_new( $post_info['_postModified'], $post_info['_postDate'] );
		}
		// post is not previously published but has been backdated.
		// (post date is edited, but save option is 'publish').
		if ( 0 === $new && ( isset( $_POST['edit_date'] ) && '1' === $_POST['edit_date'] && ! isset( $_POST['save'] ) ) ) {
			$new = 1;
		}
		// can't catch posts that were set to a past date as a draft, then published.
		$post_type_settings = get_option( 'wpt_post_types' );
		$post_types         = array_keys( $post_type_settings );
		if ( in_array( $post_type, $post_types, true ) ) {
			// identify whether limited by category/taxonomy.
			$continue = wpt_category_limit( $post_type, $post_info, $post_ID );
			if ( false === $continue ) {
				wpt_mail( '4b: XPoster Pro: Limited by term filters', 'This post was rejected by a taxonomy/term filter', $post_ID );
				return false;
			}
			// create status update and ID whether current action is edit or new.
			$ct = get_post_meta( $post_ID, '_jd_twitter', true );
			if ( isset( $_POST['_jd_twitter'] ) && '' !== trim( $_POST['_jd_twitter'] ) ) {
				$ct = sanitize_textarea_field( $_POST['_jd_twitter'] );
			}
			$custom_tweet = ( '' !== $ct ) ? stripcslashes( trim( $ct ) ) : '';
			// if ops is set and equals 'publish', this is being edited. Otherwise, it's a new post.
			if ( 0 === $new || true === $is_inline_edit ) {
				// if this is an old post and editing updates are enabled.
				if ( '1' === get_option( 'jd_tweet_default_edit' ) ) {
					$post_this = apply_filters( 'wpt_tweet_this_edit', $post_this, $_POST );
					if ( 'yes' !== $post_this ) {
						return false;
					}
				}
				wpt_mail( '4b: Post action is edit', 'This event was a post edit action.' . "\n" . 'Modified Date: ' . $post_info['_postModified'] . "\n\n" . 'Publication date:' . $post_info['_postDate'], $post_ID ); // DEBUG.
				if ( '1' === (string) $post_type_settings[ $post_type ]['post-edited-update'] || $post_this ) {
					$nptext = stripcslashes( $post_type_settings[ $post_type ]['post-edited-text'] );
					if ( ! $nptext ) {
						wpt_mail( '4b: Edited post template is empty.', 'Post Type: ' . $post_type, $post_ID ); // DEBUG.
					}

					$oldpost = true;
				}
			} else {
				wpt_mail( '4c: Post action is publish', 'This event was a post publish action.' . "\n" . 'Modified Date: ' . $post_info['_postModified'] . "\n\n" . 'Publication date:' . $post_info['_postDate'], $post_ID ); // DEBUG.
				if ( '1' === (string) $post_type_settings[ $post_type ]['post-published-update'] || $post_this ) {
					$nptext = stripcslashes( $post_type_settings[ $post_type ]['post-published-text'] );
					if ( ! $nptext ) {
						wpt_mail( '4c: Published post template is empty.', 'Post Type: ' . $post_type, $post_ID ); // DEBUG.
					}

					$newpost = true;
				}
			}
			if ( $newpost || $oldpost ) {
				$template = ( '' !== $custom_tweet ) ? $custom_tweet : $nptext;
				$sentence = wpt_truncate_status( $template, $post_info, $post_ID );
				wpt_mail( '5: Status Update Template Processed', "Template: $template; Status: $sentence", $post_ID ); // DEBUG.
				if ( function_exists( 'wpt_pro_exists' ) && true === wpt_pro_exists() ) {
					$sentence2 = wpt_truncate_status( $template, $post_info, $post_ID, false, $auth );
				}
			}
			if ( '' !== $sentence ) {
				// WPT PRO.
				if ( function_exists( 'wpt_pro_exists' ) && true === wpt_pro_exists() ) {
					$wpt_selected_users = $post_info['wpt_authorized_users'];
					// set up basic author/main account values.
					$auth_verified = ( wtt_oauth_test( $auth, 'verify' ) || wpt_mastodon_connection( $auth ) || wpt_bluesky_connection( $auth ) ) ? true : false;
					if ( empty( $wpt_selected_users ) && '1' === get_option( 'jd_individual_twitter_users' ) ) {
						$wpt_selected_users = ( $auth_verified ) ? array( $auth ) : array( false );
					}
					if ( 1 === (int) $post_info['wpt_cotweet'] || '1' !== get_option( 'jd_individual_twitter_users' ) || in_array( 'main', $wpt_selected_users, true ) ) {
						$wpt_selected_users['main'] = false;
					}
					// filter selected users before using.
					$wpt_selected_users = apply_filters( 'wpt_filter_users', $wpt_selected_users, $post_info );
					if ( '0' === (string) $post_info['wpt_delay_tweet'] || '' === $post_info['wpt_delay_tweet'] || 'on' === $post_info['wpt_no_delay'] ) {
						foreach ( $wpt_selected_users as $acct ) {
							$verified = ( wtt_oauth_test( $acct, 'verify' ) || wpt_mastodon_connection( $acct ) || wpt_bluesky_connection( $acct ) ) ? true : false;
							if ( $verified ) {
								wpt_post_to_service( $sentence2, $acct, $post_ID, $media );
							}
						}
					} else {
						foreach ( $wpt_selected_users as $acct ) {
							$acct = ( 'main' === $acct ) ? false : $acct;
							/**
							 * Filter the delay between posting and sending status updates.
							 *
							 * @hook wpt_random_delay
							 *
							 * @param {int} $rand Random integer between 60 and 480.
							 *
							 * @return {int}
							 */
							$delay    = apply_filters( 'wpt_random_delay', wp_rand( 60, 480 ) );
							$offset   = ( $auth !== $acct ) ? $delay : 0;
							$verified = ( wtt_oauth_test( $acct, 'verify' ) || wpt_mastodon_connection( $acct ) || wpt_bluesky_connection( $acct ) ) ? true : false;
							if ( $verified ) {
								$time = apply_filters( 'wpt_schedule_delay', ( (int) $post_info['wpt_delay_tweet'] ) * 60, $acct );

								/**
								 * Render the template of a scheduled status update only at the time it's sent.
								 *
								 * @hook wpt_postpone_rendering
								 * @param {bool} $postpone True to postpone rendering.
								 *
								 * @return {bool}
								 */
								$postpone_rendering = apply_filters( 'wpt_postpone_rendering', get_option( 'wpt_postpone_rendering', 'false' ) );
								if ( 'false' !== $postpone_rendering ) {
									$sentence = $template;
								}
								wp_schedule_single_event(
									time() + $time + $offset,
									'wpt_schedule_tweet_action',
									array(
										'id'       => $acct,
										'sentence' => $sentence,
										'rt'       => 0,
										'post_id'  => $post_ID,
									)
								);
								if ( WPT_DEBUG ) {
									$author_id = ( $acct ) ? "#$acct" : 'Main';
									wpt_mail(
										"7a: Status update Scheduled for author: $author_id",
										print_r(
											array(
												'id'       => $acct,
												'sentence' => $sentence,
												'rt'       => 0,
												'post_id'  => $post_ID,
												'timestamp' => time() + $time + $offset . ', ' . gmdate( 'Y-m-d H:i:s', time() + $time + $offset ),
												'current_time' => time() . ', ' . gmdate( 'Y-m-d H:i:s', time() ),
												'timezone' => get_option( 'gmt_offset' ),
												'users'    => $wpt_selected_users,
											),
											1
										),
										$post_ID
									);
									// DEBUG.
								}
							}
						}
					}
					// This cycle handles scheduling the automatic retweets.
					if ( 0 !== (int) $post_info['wpt_retweet_after'] && 'on' !== $post_info['wpt_no_repost'] ) {
						$repeat = $post_info['wpt_retweet_repeat'];
						$first  = true;
						foreach ( $wpt_selected_users as $acct ) {
							$verified = ( wtt_oauth_test( $acct, 'verify' ) || wpt_mastodon_connection( $acct ) || wpt_bluesky_connection( $acct ) ) ? true : false;
							if ( $verified ) {
								for ( $i = 1; $i <= $repeat; $i++ ) {
									$continue = apply_filters( 'wpt_allow_reposts', true, $i, $post_ID, $acct );
									if ( $continue ) {
										$retweet = apply_filters( 'wpt_set_retweet_text', $template, $i, $post_ID );

										/**
										 * Render the template of a scheduled status only at the time it's sent.
										 *
										 * @hook wpt_postpone_rendering
										 * @param {bool} $postpone True to postpone rendering.
										 *
										 * @return {bool}
										 */
										$postpone_rendering = apply_filters( 'wpt_postpone_rendering', get_option( 'wpt_postpone_rendering', 'false' ) );
										if ( 'false' !== $postpone_rendering ) {
											$retweet = $retweet;
										} else {
											$retweet = wpt_truncate_status( $retweet, $post_info, $post_ID, true, $acct );
										}
										if ( '' === $retweet ) {
											// If a filter sets this value to empty, exit without scheduling.
											return $post_ID;
										}
										// add original delay to schedule.
										$delay = ( isset( $post_info['wpt_delay_tweet'] ) ) ? ( (int) $post_info['wpt_delay_tweet'] ) * 60 : 0;
										// Don't delay the first status update of the group.
										$offset = ( true === $first ) ? 0 : wp_rand( 60, 240 ); // delay each co-tweet by 1-4 minutes.
										$time   = apply_filters( 'wpt_schedule_retweet', ( $post_info['wpt_retweet_after'] ) * ( 60 * 60 ) * $i, $acct, $i, $post_info );
										wp_schedule_single_event(
											time() + $time + $offset + $delay,
											'wpt_schedule_tweet_action',
											array(
												'id'       => $acct,
												'sentence' => $retweet,
												'rt'       => $i,
												'post_id'  => $post_ID,
											)
										);
										if ( WPT_DEBUG ) {
											if ( $acct ) {
												$author_id = "#$acct";
											} else {
												$author_id = 'Main';
											}
											wpt_mail(
												"7b: Retweet Scheduled for author $author_id",
												print_r(
													array(
														'id'         => $acct,
														'sentence'   => array( $retweet, $i, $post_ID ),
														'timestamp'  => time() + $time + $offset + $delay,
														'time'       => array( $time, $offset, $delay, get_option( 'gmt_offset' ), time() ),
														'timestring' => gmdate( 'Y-m-d H:i:s', time() + $time + $offset + $delay ),
														'current_ts' => gmdate( 'Y-m-d H:i:s', time() ),
													),
													1
												),
												$post_ID
											); // DEBUG.
										}
										$tweet_limit = (int) apply_filters( 'wpt_tweet_repeat_limit', 4, $post_ID );
										if ( $i === $tweet_limit ) {
											break;
										}
									}
								}
							}
							$first = false;
						}
					}
				} else {
					wpt_post_to_service( $sentence, false, $post_ID, $media );
				}
				// END WPT PRO.
			}
		}
	}

	return $post_ID;
}

/**
 *  Send updates on links in link manager. Only active if Link plug-in is installed.
 *
 * @param integer $link_id Database ID for link.
 *
 * @return mixed boolean/integer link ID if successful, false if failure.
 */
function wpt_post_update_link( $link_id ) {
	wpt_check_version();
	$thislinkprivate = sanitize_text_field( $_POST['link_visible'] );
	if ( 'N' !== $thislinkprivate ) {
		$thislinkname        = stripslashes( sanitize_text_field( $_POST['link_name'] ) );
		$thispostlink        = sanitize_text_field( $_POST['link_url'] );
		$thislinkdescription = stripcslashes( sanitize_textarea_field( $_POST['link_description'] ) );
		$sentence            = stripcslashes( get_option( 'newlink-published-text' ) );
		$sentence            = str_ireplace( '#title#', $thislinkname, $sentence );
		$sentence            = str_ireplace( '#description#', $thislinkdescription, $sentence );

		if ( mb_strlen( $sentence ) > 118 ) {
			$sentence = mb_substr( $sentence, 0, 114 ) . '...';
		}
		/**
		 * Customize the URL shortening of a link in the link manager.
		 *
		 * @hook wptt_shorten_link
		 *
		 * @param {string} $thispostlink The passed bookmark link.
		 * @param {string} $thislinkname The provided link title.
		 * @param {bool}   $post_ID False, because links don't have post IDs.
		 * @param {bool}   $test 'link' to indicate a link is being shortened.
		 *
		 * @return {string}
		 */
		$shrink = apply_filters( 'wptt_shorten_link', $thispostlink, $thislinkname, false, 'link' );
		if ( false === stripos( $sentence, '#url#' ) ) {
			$sentence = $sentence . ' ' . $shrink;
		} else {
			$sentence = str_ireplace( '#url#', $shrink, $sentence );
		}

		if ( false === stripos( $sentence, '#longurl#' ) ) {
			$sentence = $sentence . ' ' . $thispostlink;
		} else {
			$sentence = str_ireplace( '#longurl#', $thispostlink, $sentence );
		}

		if ( '' !== $sentence ) {
			wpt_post_to_service( $sentence, false, $link_id );
		}

		return $link_id;
	} else {
		return false;
	}
}

/**
 * Generate hash tags from tags set on post.
 *
 * @param int $post_ID Post ID.
 *
 * @return string $hashtags Hashtags in format needed for status updates.
 */
function wpt_generate_hash_tags( $post_ID ) {
	$hashtags       = '';
	$term_meta      = false;
	$t_id           = false;
	$max_tags       = get_option( 'jd_max_tags', '3' );
	$max_characters = get_option( 'jd_max_characters', '20' );
	$max_characters = ( '0' === $max_characters || '' === $max_characters ) ? 100 : $max_characters + 1;
	if ( '0' === $max_tags || '' === $max_tags ) {
		$max_tags = 100;
	}
	$use_cats = ( '1' === get_option( 'wpt_use_cats' ) ) ? true : false;
	$tags     = ( true === $use_cats ) ? wp_get_post_categories( $post_ID, array( 'fields' => 'all' ) ) : get_the_tags( $post_ID );
	/**
	 * Change the taxonomy used by default to generate post tags. Array of terms attached to post.
	 *
	 * @hook wpt_hash_source
	 * @param {array} $tags Array of post terms.
	 * @param {int}   $post_ID Post ID.
	 *
	 * @return {array}
	 */
	$tags = apply_filters( 'wpt_hash_source', $tags, $post_ID );
	if ( $tags && count( $tags ) > 0 ) {
		$i = 1;
		foreach ( $tags as $value ) {
			if ( function_exists( 'wpt_pro_exists' ) ) {
				$t_id      = $value->term_id;
				$term_meta = get_option( "wpt_taxonomy_$t_id" );
			}
			$source = get_option( 'wpt_tag_source' );
			if ( 'slug' === $source ) {
				// If the tag has an '@' symbol as the first character, assume it is a mention unless set.
				if ( 0 === stripos( $value->name, '@' ) && ! $term_meta ) {
					$term_meta = 5;
				}
				$tag = $value->slug;
			} else {
				$tag = $value->name;
				// If the tag has an '@' symbol as the first character, assume it is a mention unless set.
				if ( 0 === stripos( $value->name, '@' ) && ! $term_meta ) {
					$term_meta = 4;
				}
			}
			$strip   = get_option( 'jd_strip_nonan' );
			$search  = '/[^\p{L}\p{N}\s]/u';
			$replace = get_option( 'jd_replace_character' );
			$replace = ( '[ ]' === $replace || '' === $replace ) ? '' : $replace;
			if ( false !== strpos( $tag, ' ' ) ) {
				// If multiple words, camelcase tag.
				$tag = ucwords( $tag );
			}
			$tag = str_ireplace( ' ', $replace, trim( $tag ) );
			$tag = preg_replace( '/[\/]/', $replace, $tag ); // remove forward slashes.
			$tag = ( '1' === $strip ) ? preg_replace( $search, $replace, $tag ) : $tag;

			switch ( $term_meta ) {
				case 1:
					$newtag = "#$tag";
					break;
				case 2:
					$newtag = "$$tag";
					break;
				case 3:
					$newtag = '';
					break;
				case 4:
					$newtag = $tag;
					break;
				case 5:
					$newtag = "@$tag";
					break;
				default:
					/**
					 * Change the default tag character. Default '#'.
					 *
					 * @hook wpt_tag_default
					 * @param {string} $char Character used to convert tags into hashtags.
					 * @param {int}    $t_id Term ID.
					 *
					 * @return {string}
					 */
					$newtag = apply_filters( 'wpt_tag_default', '#', $t_id ) . $tag;
			}
			if ( mb_strlen( $newtag ) > 2 && ( mb_strlen( $newtag ) <= $max_characters ) && ( $i <= $max_tags ) ) {
				$hashtags .= "$newtag ";
				++$i;
			}
		}
	}
	$hashtags = trim( $hashtags );
	if ( mb_strlen( $hashtags ) <= 1 ) {
		$hashtags = '';
	}

	return $hashtags;
}

add_action( 'admin_menu', 'wpt_add_twitter_outer_box' );
/**
 * Set up post meta box.
 */
function wpt_add_twitter_outer_box() {
	wpt_check_version();
	// add X.com panel to post types where it's enabled.
	$wpt_post_types = get_option( 'wpt_post_types' );
	if ( is_array( $wpt_post_types ) ) {
		foreach ( $wpt_post_types as $key => $value ) {
			if ( '1' === (string) $value['post-published-update'] || '1' === (string) $value['post-edited-update'] ) {
				if ( current_user_can( 'wpt_can_tweet' ) ) {
					add_meta_box( 'wp2t', 'XPoster', 'wpt_add_twitter_inner_box', $key, 'side' );
				}
			}
		}
	}
}

add_action( 'admin_menu', 'wpt_add_twitter_debug_box' );
/**
 * Set up post meta box.
 */
function wpt_add_twitter_debug_box() {
	if ( WPT_DEBUG && current_user_can( 'manage_options' ) ) {
		wpt_check_version();
		// add X.com panel to post types where it's enabled.
		$wpt_post_types = wpt_allowed_post_types();
		foreach ( $wpt_post_types as $type ) {
			add_meta_box( 'wp2t-debug', 'XPoster Debugging', 'wpt_show_debug', $type, 'advanced' );
		}
	}
}

/**
 * Print post meta box
 *
 * @param  object $post Post object.
 */
function wpt_add_twitter_inner_box( $post ) {
	$nonce = wp_create_nonce( 'wp-to-twitter-nonce' );
	?>
	<div>
		<input type="hidden" name="wp_to_twitter_nonce" value="<?php echo $nonce; ?>">
		<input type="hidden" name="wp_to_twitter_meta" value="true">
	</div>
	<?php
	if ( current_user_can( 'wpt_can_tweet' ) ) {
		$is_pro = ( function_exists( 'wpt_pro_exists' ) ) ? 'pro' : 'free';
		?>
		<div class='wp-to-twitter <?php echo $is_pro; ?>'>
		<?php
		$options = get_option( 'wpt_post_types' );
		$status  = $post->post_status;
		wpt_show_metabox_message( $post, $options );
		// Show switch to flip update status.
		$switch = wpt_show_post_switch( $post, $options );
		echo $switch;
		echo '<div class="wpt-options-metabox">';
		$user_tweet = apply_filters( 'wpt_user_text', '', $status );
		// Formulate Template display.
		$template = wpt_display_status_template( $post, $options );
		if ( $user_tweet ) {
			// If a user template is defined, replace the existing template.
			$template = $user_tweet;
		}
		if ( 'publish' === $status && ( current_user_can( 'wpt_tweet_now' ) || current_user_can( 'manage_options' ) ) ) {
			// Show metabox status buttons.
			$buttons = wpt_display_metabox_status_buttons( $is_pro );
			echo $buttons;
		}
		if ( current_user_can( 'wpt_twitter_custom' ) || current_user_can( 'manage_options' ) ) {
			$custom_update = get_post_meta( $post->ID, '_jd_twitter', true );
			?>
			<p class='jtw'>
				<label for="wpt_custom_tweet"><?php _e( 'Custom Status Update', 'wp-to-twitter' ); ?></label><br/>
				<textarea class="wpt_tweet_box widefat" name="_jd_twitter" id="wpt_custom_tweet" placeholder="<?php echo esc_attr( $template ); ?>" rows="2" cols="60"><?php echo esc_textarea( stripslashes( $custom_update ) ); ?></textarea>
				<?php echo apply_filters( 'wpt_custom_box', '', $template, $post->ID ); ?>
			</p>
			<div class="wpt-template-resources wpt-flex">
				<p class='wpt-template'>
					<?php _e( 'Default template:', 'wp-to-twitter' ); ?><br /><code><?php echo stripcslashes( $template ); ?></code>
					<?php echo apply_filters( 'wpt_template_block', '', $template, $post->ID ); ?>
				</p>
				<div class='wptab' id='notes'>
					<h3><?php _e( 'Template Tags', 'wp-to-twitter' ); ?></h3>
					<ul class="inline-list">
					<?php
					$tags = wpt_tags();
					foreach ( $tags as $tag ) {
						$pressed = ( false === stripos( $template, '#' . $tag . '#' ) ) ? 'false' : 'true';
						echo '<li><button type="button" class="button-secondary" aria-pressed="' . $pressed . '">#' . $tag . '#</button></li>';
					}
					do_action( 'wpt_notes_tab', $post->ID );
					?>
					</ul>
				</div>
			</div>
			<?php
			$retweet_fields = apply_filters( 'wpt_custom_retweet_fields', '', $post->ID );
			echo $retweet_fields;
			if ( get_option( 'jd_keyword_format' ) === '2' ) {
				$custom_keyword = get_post_meta( $post->ID, '_yourls_keyword', true );
				echo "<label for='yourls_keyword'>" . __( 'YOURLS Custom Keyword', 'wp-to-twitter' ) . "</label> <input type='text' name='_yourls_keyword' id='yourls_keyword' value='$custom_keyword' />";
			}
		} else {
			?>
			<input type="hidden" name='_jd_twitter' value='<?php echo esc_attr( $template ); ?>' />
			<p class='wpt-template'>
				<?php _e( 'Template:', 'wp-to-twitter' ); ?> <code><?php echo stripcslashes( $template ); ?></code>
				<?php echo apply_filters( 'wpt_template_block', '', $template, $post->ID ); ?>
			</p>
			<?php
		}
		?>
		<div class='wpt-options'>
			<div class='wptab' id='custom'>
			<?php
			// XPoster Pro.
			if ( 'pro' === $is_pro && ( current_user_can( 'wpt_twitter_custom' ) || current_user_can( 'manage_options' ) ) ) {
				wpt_schedule_values( $post->ID );
				do_action( 'wpt_custom_tab', $post->ID, 'visible' );
				if ( current_user_can( 'edit_others_posts' ) ) {
					if ( '1' === get_option( 'jd_individual_twitter_users' ) ) {
						$selected = ( get_post_meta( $post->ID, '_wpt_authorized_users', true ) ) ? get_post_meta( $post->ID, '_wpt_authorized_users', true ) : array();
						if ( function_exists( 'wpt_authorized_users' ) ) {
							echo wpt_authorized_users( $selected );
							do_action( 'wpt_authors_tab', $post->ID, $selected );
						}
					}
				}
			}
			if ( ! current_user_can( 'wpt_twitter_custom' ) && ! current_user_can( 'manage_options' ) ) {
				?>
				<p><?php _e( 'Customizing XPoster options is not allowed for your user role.', 'wp-to-twitter' ); ?></p>
				<?php
				if ( 'pro' === $is_pro ) {
					wpt_schedule_values( $post->ID, 'hidden' );
					do_action( 'wpt_custom_tab', $post->ID, 'hidden' );
				}
			}
			?>
			</div>
		</div>
		<?php wpt_show_history( $post->ID ); ?>
		<?php wpt_meta_box_support( $is_pro ); ?>
		</div>
		</div>
		<?php
	} else {
		// permissions: this user isn't allowed to post status updates.
		_e( 'Your role does not have the ability to post status updates from this site.', 'wp-to-twitter' );
		?>
		<input type='hidden' name='_wpt_post_this' value='no'/>
		<?php
	}
}

/**
 * Format history of status updates attempted on current post.
 *
 * @param array $post_id Post ID to fetch status updates on.
 */
function wpt_show_history( $post_id ) {
	$previous_tweets = get_post_meta( $post_id, '_jd_wp_twitter', true );
	$failed_tweets   = get_post_meta( $post_id, '_wpt_failed' );

	if ( ! is_array( $previous_tweets ) && '' !== $previous_tweets ) {
		$previous_tweets = array( 0 => $previous_tweets );
	}
	if ( ! empty( $previous_tweets ) || ! empty( $failed_tweets ) ) {
		?>
	<p class='panel-toggle'>
		<button type="button" aria-expanded="false" class='history-toggle button-secondary'><span class='dashicons dashicons-plus' aria-hidden="true"></span><?php _e( 'View Update History', 'wp-to-twitter' ); ?></button>
	</p>
	<div class='history'>
	<h4 class='wpt-past-updates'><em><?php _e( 'Previous Updates', 'wp-to-twitter' ); ?>:</em></h4>
	<ul>
		<?php
		$has_history   = false;
		$hidden_fields = '';
		if ( is_array( $previous_tweets ) ) {
			foreach ( $previous_tweets as $previous_tweet ) {
				if ( '' !== $previous_tweet ) {
					$has_history     = true;
					$twitter_intent  = '';
					$mastodon_intent = '';
					$bluesky_intent  = '';
					if ( wtt_oauth_test() ) {
						$twitter_intent = "<a href='https://x.com/intent/tweet?text=" . urlencode( $previous_tweet ) . "'>" . __( 'Repost on X.com', 'wp-to-twitter' ) . '</a>';
					}
					if ( wpt_mastodon_connection() ) {
						$mastodon        = get_option( 'wpt_mastodon_instance' );
						$mastodon_intent = "<a href='" . esc_url( $mastodon ) . '/statuses/new?text=' . urlencode( $previous_tweet ) . "'>" . __( 'Repost on Mastodon', 'wp-to-twitter' ) . '</a>';
					}
					if ( wpt_bluesky_connection() ) {
						$bluesky_intent = "<a href='https://bsky.app/intent/compose?text=" . urlencode( $previous_tweet ) . "'>" . __( 'Repost on Bluesky', 'wp-to-twitter' ) . '</a>';
					}
					$hidden_fields .= "<input type='hidden' name='_jd_wp_twitter[]' value='" . esc_attr( $previous_tweet ) . "' />";
					echo "<li>$previous_tweet $twitter_intent $mastodon_intent $bluesky_intent</li>";
				}
			}
		}
		?>
	</ul>
		<?php
		$list       = false;
		$error_list = '';
		if ( is_array( $failed_tweets ) ) {
			foreach ( $failed_tweets as $failed_tweet ) {
				if ( ! empty( $failed_tweet ) ) {
					$ft     = $failed_tweet['sentence'];
					$reason = $failed_tweet['code'];
					$error  = $failed_tweet['error'];
					$list   = true;

					$twitter_intent  = '';
					$mastodon_intent = '';
					$bluesky_intent  = '';
					if ( wtt_oauth_test() ) {
						$twitter_intent = "<a href='https://x.com/intent/tweet?text=" . urlencode( $ft ) . "'>" . __( 'Send to X.com', 'wp-to-twitter' ) . '</a>';
					}
					if ( wpt_mastodon_connection() ) {
						$mastodon        = get_option( 'wpt_mastodon_instance' );
						$mastodon_intent = "<a href='" . esc_url( $mastodon ) . '/statuses/new?text=' . urlencode( $ft ) . "'>" . __( 'Send to Mastodon', 'wp-to-twitter' ) . '</a>';
					}
					if ( wpt_bluesky_connection() ) {
						$bluesky_intent = "<a href='https://bsky.app/intent/compose?text=" . urlencode( $ft ) . "'>" . __( 'Send to Bluesky', 'wp-to-twitter' ) . '</a>';
					}
					$error_list .= "<li><code>Error: $reason</code> $ft $twitter_intent $mastodon_intent $bluesky_intent <br /><em>$error</em></li>";
				}
			}
			if ( true === $list ) {
				echo "<h4 class='wpt-failed-updates'><em>" . __( 'Failed Status Updates', 'wp-to-twitter' ) . ":</em></h4>
				<ul>$error_list</ul>";
			}
		}
		echo '<div>' . $hidden_fields . '</div>';
		if ( $has_history || $list ) {
			echo "<p><input type='checkbox' name='wpt_clear_history' id='wptch' value='clear' /> <label for='wptch'>" . __( 'Delete Status History', 'wp-to-twitter' ) . '</label></p>';
		}
		?>
	</div>
		<?php
	}
}

add_action( 'admin_enqueue_scripts', 'wpt_admin_scripts', 10, 1 );
/**
 * Enqueue admin scripts for XPoster and XPoster PRO.
 */
function wpt_admin_scripts() {
	global $current_screen, $wpt_version;
	if ( SCRIPT_DEBUG ) {
		$wpt_version .= '-' . wp_rand( 10000, 99999 );
	}
	wp_register_script( 'wpt.charcount', plugins_url( 'js/jquery.charcount.js', __FILE__ ), array( 'jquery' ), $wpt_version );
	if ( 'post' === $current_screen->base || 'xposter-pro_page_wp-to-twitter-schedule' === $current_screen->id ) {
		wp_enqueue_script( 'wpt.charcount' );
		wp_register_style( 'wpt-post-styles', plugins_url( 'css/post-styles.css', __FILE__ ), array(), $wpt_version );
		wp_enqueue_style( 'wpt-post-styles' );
		$config = wpt_max_length();
		// add one; character count starts from 1.
		if ( 'post' === $current_screen->base ) {
			$allowed = $config['base_length'] - mb_strlen( stripslashes( get_option( 'jd_twit_prepend' ) . get_option( 'jd_twit_append' ) ) ) + 1;
		} else {
			$allowed = $config['base_length'] + 1;
		}
		if ( function_exists( 'wpt_pro_exists' ) ) {
			$first = '#custom';
		} else {
			$first = '#notes';
		}
		wp_register_script( 'wpt-base-js', plugins_url( 'js/base.js', __FILE__ ), array( 'jquery', 'wpt.charcount' ), $wpt_version, true );
		wp_enqueue_script( 'wpt-base-js' );
		wp_localize_script(
			'wpt-base-js',
			'wptSettings',
			array(
				'allowed' => $allowed,
				'first'   => $first,
				'is_ssl'  => ( wpt_is_ssl( home_url() ) ) ? 'true' : 'false',
				'text'    => __( 'Characters left: ', 'wp-to-twitter' ),
				'updated' => __( 'Custom status template updated', 'wp-to-twitter' ),
			)
		);
	}
	if ( 'post' === $current_screen->base && isset( $_GET['post'] ) && ( current_user_can( 'wpt_tweet_now' ) || current_user_can( 'manage_options' ) ) ) {
		wp_enqueue_script( 'wpt.ajax', plugins_url( 'js/ajax.js', __FILE__ ), array( 'jquery' ), $wpt_version );
		wp_localize_script(
			'wpt.ajax',
			'wpt_data',
			array(
				'post_ID'  => (int) $_GET['post'],
				'action'   => 'wpt_post_update',
				'security' => wp_create_nonce( 'wpt-tweet-nonce' ),
			)
		);
	}
	if ( 'settings_page_wp-to-twitter/wp-to-twitter' === $current_screen->id || 'toplevel_page_wp-tweets-pro' === $current_screen->id ) {
		wp_enqueue_script( 'wpt.tabs', plugins_url( 'js/tabs.js', __FILE__ ), array( 'jquery' ), $wpt_version );
		wp_localize_script(
			'wpt.tabs',
			'wpt',
			array(
				'firstItem' => 'wpt_post',
				'firstPerm' => 'wpt_editor',
			)
		);
		wp_enqueue_script( 'dashboard' );
	}
}

add_action( 'wp_ajax_wpt_post_update', 'wpt_ajax_tweet' );
/**
 * Handle updates sent via Ajax Update Now/Schedule Update buttons.
 */
function wpt_ajax_tweet() {
	if ( ! check_ajax_referer( 'wpt-tweet-nonce', 'security', false ) ) {
		wp_die( __( 'XPoster: Invalid Security Check', 'wp-to-twitter' ) );
	}
	$action       = ( 'tweet' === $_REQUEST['tweet_action'] ) ? 'tweet' : 'schedule';
	$authors      = ( isset( $_REQUEST['tweet_auth'] ) && null !== $_REQUEST['tweet_auth'] ) ? map_deep( $_REQUEST['tweet_auth'], 'sanitize_text_field' ) : false;
	$upload       = ( isset( $_REQUEST['tweet_upload'] ) && null !== $_REQUEST['tweet_upload'] ) ? (int) $_REQUEST['tweet_upload'] : '1';
	$current_user = wp_get_current_user();
	if ( function_exists( 'wpt_pro_exists' ) && wpt_pro_exists() ) {
		$acct     = $current_user->ID;
		$verified = ( wtt_oauth_test( $acct, 'verify' ) || wpt_mastodon_connection( $acct ) || wpt_bluesky_connection( $acct ) ) ? true : false;
		if ( $verified ) {
			$auth    = $current_user->ID;
			$user_ID = $current_user->ID;
		} else {
			$auth    = false;
			$user_ID = $current_user->ID;
		}
	} else {
		$auth    = false;
		$user_ID = $current_user->ID;
	}
	$authors = ( is_array( $authors ) && ! empty( $authors ) ) ? $authors : array( $auth );

	if ( current_user_can( 'wpt_can_tweet' ) ) {
		$options        = get_option( 'wpt_post_types' );
		$post_ID        = intval( $_REQUEST['tweet_post_id'] );
		$type           = get_post_type( $post_ID );
		$default        = ( isset( $options[ $type ]['post-edited-text'] ) ) ? $options[ $type ]['post-edited-text'] : '';
		$sentence       = ( isset( $_REQUEST['tweet_text'] ) && '' !== trim( $_REQUEST['tweet_text'] ) ) ? $_REQUEST['tweet_text'] : $default;
		$sentence       = stripcslashes( trim( $sentence ) );
		$post_info      = wpt_post_info( $post_ID );
		$sentence       = wpt_truncate_status( $sentence, $post_info, $post_ID, false, $user_ID );
		$schedule       = ( isset( $_REQUEST['tweet_schedule'] ) ) ? strtotime( $_REQUEST['tweet_schedule'] ) : wp_rand( 60, 240 );
		$print_schedule = date_i18n( get_option( 'date_format' ) . ' @ ' . get_option( 'time_format' ), $schedule );
		$offset         = ( 60 * 60 * get_option( 'gmt_offset' ) );
		$schedule       = $schedule - $offset;
		$media          = ( '1' === $upload ) ? false : true; // this is correct; the boolean logic is reversed. Blah.

		foreach ( $authors as $auth ) {
			$auth = ( 'main' === $auth ) ? false : $auth;
			switch ( $action ) {
				case 'tweet':
					wpt_post_to_service( $sentence, $auth, $post_ID, $media );
					break;
				case 'schedule':
					wp_schedule_single_event(
						$schedule,
						'wpt_schedule_tweet_action',
						array(
							'id'       => $auth,
							'sentence' => $sentence,
							'rt'       => 0,
							'post_id'  => $post_ID,
						)
					);
					break;
			}
			$log     = wpt_get_log( 'wpt_status_message', $post_ID );
			$message = is_array( $log ) ? $log['message'] : $log;
			// Translators: Full text of Update, time scheduled for.
			$return = ( 'tweet' === $action ) ? $message : sprintf( __( 'Update scheduled: %1$s for %2$s', 'wp-to-twitter' ), '"' . $sentence . '"', $print_schedule );
			echo $return;
			if ( count( $authors ) > 1 ) {
				echo '<br />';
			}
		}
	} else {
		echo __( 'You are not authorized to perform this action', 'wp-to-twitter' );
	}
	die;
}

/**
 * Post the Custom Update & custom Update data into the post meta table
 *
 * @param integer $id Post ID.
 * @param object  $post Post object.
 *
 * @return bool
 */
function wpt_save_post( $id, $post ) {
	if ( empty( $_POST ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $id ) || isset( $_POST['_inline_edit'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ! wpt_in_post_type( $id ) ) {
		return $id;
	}
	if ( isset( $_POST['wp_to_twitter_meta'] ) ) {
		$nonce = ( isset( $_POST['wp_to_twitter_nonce'] ) ) ? $_POST['wp_to_twitter_nonce'] : false;
		if ( ! ( $nonce && wp_verify_nonce( $nonce, 'wp-to-twitter-nonce' ) ) ) {
			wp_die( 'XPoster: Security check failed' );
		}
		if ( isset( $_POST['_yourls_keyword'] ) ) {
			$yourls = sanitize_text_field( $_POST['_yourls_keyword'] );
			update_post_meta( $id, '_yourls_keyword', $yourls );
		}
		if ( isset( $_POST['_jd_twitter'] ) && '' !== $_POST['_jd_twitter'] ) {
			$twitter = sanitize_textarea_field( $_POST['_jd_twitter'] );
			update_post_meta( $id, '_jd_twitter', $twitter );
		} elseif ( isset( $_POST['_jd_twitter'] ) && '' === $_POST['_jd_twitter'] ) {
			delete_post_meta( $id, '_jd_twitter' );
		}
		if ( isset( $_POST['_jd_wp_twitter'] ) && '' !== $_POST['_jd_wp_twitter'] ) {
			$wp_twitter = sanitize_textarea_field( $_POST['_jd_wp_twitter'] );
			update_post_meta( $id, '_jd_wp_twitter', $wp_twitter );
		}
		if ( isset( $_POST['_wpt_post_this'] ) ) {
			$post_this = ( 'no' === $_POST['_wpt_post_this'] ) ? 'no' : 'yes';
			update_post_meta( $id, '_wpt_post_this', $post_this );
		} else {
			$post_default = ( '1' === get_option( 'jd_tweet_default' ) ) ? 'no' : 'yes';
			update_post_meta( $id, '_wpt_post_this', $post_default );
		}
		if ( isset( $_POST['wpt_clear_history'] ) && 'clear' === $_POST['wpt_clear_history'] ) {
			delete_post_meta( $id, '_wpt_failed' );
			delete_post_meta( $id, '_jd_wp_twitter' );
			delete_post_meta( $id, '_wpt_short_url' );
			delete_post_meta( $id, '_wp_jd_twitter' );
		}
		/**
		 * Runs when post data is inserted.
		 *
		 * @hook wpt_insert_post
		 *
		 * @param {array} $_POST Unaltered POST data.
		 * @param {int}   $id Post ID
		 */
		do_action( 'wpt_insert_post', $_POST, $id );
		// WPT PRO.
		// only send debug data if post meta is updated.
		wpt_mail( 'Post Meta Processed', 'XPoster post meta was updated' . "\n\n" . print_r( map_deep( $_POST, 'sanitize_textarea_field' ), 1 ), $id ); // DEBUG.

		if ( isset( $_POST['wpt-delete-debug'] ) && 'true' === $_POST['wpt-delete-debug'] ) {
			delete_post_meta( $id, '_wpt_debug_log' );
		}
		if ( isset( $_POST['wpt-delete-all-debug'] ) && 'true' === $_POST['wpt-delete-all-debug'] ) {
			delete_post_meta_by_key( '_wpt_debug_log' );
		}
	}
	return $id;
}

add_action( 'init', 'wpt_old_admin_redirect' );
/**
 * Send links to old admin to new admin page
 */
function wpt_old_admin_redirect() {
	if ( is_admin() && isset( $_GET['page'] ) && 'wp-to-twitter/wp-to-twitter.php' === $_GET['page'] ) {
		wp_safe_redirect( admin_url( 'admin.php?page=wp-tweets-pro' ) );
		exit;
	}
}

add_action( 'admin_menu', 'wpt_admin_page' );
/**
 * Add the administrative settings to the "Settings" menu.
 */
function wpt_admin_page() {
	if ( function_exists( 'add_menu_page' ) && ! function_exists( 'wpt_pro_functions' ) ) {
		add_menu_page( 'XPoster', 'XPoster', 'manage_options', 'wp-tweets-pro', 'wpt_update_settings', 'dashicons-share' );
	}
}

add_action( 'admin_head', 'wpt_admin_style' );
/**
 * Add stylesheets to XPoster pages.
 */
function wpt_admin_style() {
	global $wpt_version;
	if ( SCRIPT_DEBUG ) {
		$wpt_version .= '-' . wp_rand( 10000, 99999 );
	}
	global $current_screen;

	if ( isset( $_GET['page'] ) && ( 'wp-to-twitter' === $_GET['page'] || 'wp-tweets-pro' === $_GET['page'] || 'wp-to-twitter-schedule' === $_GET['page'] || 'wp-to-twitter-tweets' === $_GET['page'] || 'wp-to-twitter-errors' === $_GET['page'] ) || 'profile' === $current_screen->base ) {
		wp_enqueue_style( 'wpt-styles', plugins_url( 'css/styles.css', __FILE__ ), array(), $wpt_version );
	}
}

/**
 * Add XPoster links to plug-in information.
 *
 * @param array  $links Array of links.
 * @param string $file Current file name.
 *
 * @return link new array.
 */
function wpt_plugin_action( $links, $file ) {
	if ( plugin_basename( __DIR__ . '/wp-to-twitter.php' ) === $file ) {
		$admin_url = admin_url( 'admin.php?page=wp-tweets-pro' );
		$links[]   = "<a href='$admin_url'>" . __( 'XPoster Settings', 'wp-to-twitter' ) . '</a>';
	}

	return $links;
}

// Add Plugin Actions to WordPress.
add_filter( 'plugin_action_links', 'wpt_plugin_action', 10, 2 );
add_action( 'in_plugin_update_message-wp-to-twitter/wp-to-twitter.php', 'wpt_plugin_update_message' );
/**
 * Parse plugin update info to display in update list.
 */
function wpt_plugin_update_message() {
	global $wpt_version;
	$note = '';
	define( 'WPT_PLUGIN_README_URL', 'http://svn.wp-plugins.org/wp-to-twitter/trunk/readme.txt' );
	$response = wp_remote_get( WPT_PLUGIN_README_URL, array( 'user-agent' => 'WordPress/XPoster' . $wpt_version . '; ' . get_bloginfo( 'url' ) ) );
	if ( ! is_wp_error( $response ) || is_array( $response ) ) {
		$data   = $response['body'];
		$bits   = explode( '== Upgrade Notice ==', $data );
		$notice = trim( str_replace( '* ', '', nl2br( trim( $bits[1] ) ) ) );
		if ( $notice ) {
			$note = '</div><div id="wpt-upgrade" class="notice inline notice-warning"><ul><li><strong style="color:#c22;">Upgrade Notes:</strong> ' . str_replace( '* ', '', nl2br( trim( $bits[1] ) ) ) . '</li></ul>';
		}
	}

	echo $note;
}

if ( '1' === get_option( 'jd_twit_blogroll' ) ) {
	add_action( 'add_link', 'wpt_post_update_link' );
}

if ( function_exists( 'wp_after_insert_post' ) ) {
	/**
	 * Use the `wp_after_insert_post` action to run Updates.
	 *
	 * @since WordPress 5.6
	 */
	add_action( 'wp_after_insert_post', 'wpt_save_post', 10, 2 );
	add_action( 'wp_after_insert_post', 'wpt_do_post_update', 15, 4 );
} else {
	add_action( 'save_post', 'wpt_save_post', 10, 2 );
	add_action( 'save_post', 'wpt_do_post_update', 15 );
}
/**
 * Check whether a given post is in an allowed post type and has an update template configured.
 *
 * @param integer $id Post ID.
 *
 * @return boolean True if post is allowed, false otherwise.
 */
function wpt_in_post_type( $id ) {
	$post_types = wpt_allowed_post_types();
	$type       = get_post_type( $id );
	if ( in_array( $type, $post_types, true ) ) {
		return true;
	}
	if ( WPT_DEBUG ) {
		wpt_mail( '0: Not an updated post type', 'This post type is not enabled for status updates: ' . $type, $id );
	}

	return false;
}

/**
 * Get array of post types that can be updated.
 *
 * @return array
 */
function wpt_allowed_post_types() {
	$post_type_settings = get_option( 'wpt_post_types' );
	$allowed_types      = array();
	if ( is_array( $post_type_settings ) && ! empty( $post_type_settings ) ) {
		foreach ( $post_type_settings as $type => $settings ) {
			if ( '1' === (string) $settings['post-edited-update'] || '1' === (string) $settings['post-published-update'] ) {
				$allowed_types[] = $type;
			}
		}
	}

	/**
	 * Return array of post types that can be sent as status updates.
	 *
	 * @hook wpt_allowed_post_types
	 * @param {array} $types Array of post type names enabled for status updates either when editing or publishing.
	 * @param {array} $post_type_settings Multidimensional array of post types and post type settings.
	 *
	 * @return {array}
	 */
	return apply_filters( 'wpt_allowed_post_types', $allowed_types, $post_type_settings );
}

add_action( 'future_to_publish', 'wpt_future_to_publish', 16 );
/**
 * Handle updating posts scheduled for the future.
 *
 * @param object $post Post object.
 */
function wpt_future_to_publish( $post ) {
	$id = $post->ID;
	if ( wp_is_post_autosave( $id ) || wp_is_post_revision( $id ) || ! wpt_in_post_type( $id ) ) {
		return;
	}
	wpt_mail( 'Transitioning future to publish', $id );
	wpt_post_update_future( $id );
}

/**
 * Check whether autotweeting has been allowed.
 *
 * @param int $post_id Post ID.
 *
 * @return bool
 */
function wpt_auto_tweet_allowed( $post_id ) {
	$state  = get_option( 'wpt_auto_tweet_allowed', '0' );
	$return = ( '0' !== $state ) ? true : false;

	/**
	 * Return true if auto tweeting of old posts is enabled.
	 *
	 * @hook wpt_auto_tweet_allowed
	 * @param {bool} $return true if enabled.
	 * @param {int}  $post_id Post ID.
	 *
	 * @return {bool}
	 */
	return apply_filters( 'wpt_auto_tweet_allowed', $return, $post_id );
}

/**
 * Handle updating posts published directly. As of 12/10/2020, supports new wp_after_insert_post to improve support when used with block editor.
 *
 * @param int     $id Post ID.
 * @param object  $post Post object.
 * @param boolean $updated True if updated, false if inserted.
 * @param object  $post_before The post prior to this update, or null for new posts.
 */
function wpt_do_post_update( $id, $post = null, $updated = null, $post_before = null ) {
	if ( ( empty( $_POST ) && ! wpt_auto_tweet_allowed( $id ) ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $id ) || isset( $_POST['_inline_edit'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX && ! wpt_auto_tweet_allowed( $id ) ) || ! wpt_in_post_type( $id ) ) {
		return $id;
	}

	$post = ( null === $post ) ? get_post( $id ) : $post;
	if ( 'publish' !== $post->post_status ) {
		return $id;
	}
	// is there any reason to accept any other status?
	wpt_mail( 'Status update on published post', $id );
	wpt_post_update_instant( $id, $post, $updated, $post_before );
}

add_action( 'xmlrpc_publish_post', 'wpt_post_update_xmlrpc' );
add_action( 'publish_phone', 'wpt_post_update_xmlrpc' );

/**
 * For future posts, check transients to see whether this post has already been published. Prevents duplicate status update attempts in older versions of WP.
 *
 * @param integer $id Post ID.
 */
function wpt_post_update_future( $id ) {
	set_transient( '_wpt_post_update_future', $id, 10 );
	// instant action has already run for this post.
	// prevent running actions twice (need both for older WP).
	if ( get_transient( '_wpt_post_update_instant' ) && (int) get_transient( '_wpt_twit_instant' ) === $id ) {
		delete_transient( '_wpt_post_update_instant' );

		return;
	}

	wpt_post_update( $id, 'future' );
}

/**
 * For immediate posts, check transients to see whether this post has already been published. Prevents duplicate status update attempts in older versions of WP or cases where a future action is being run after the initial action.
 *
 * @param int     $id Post ID.
 * @param object  $post Post object.
 * @param boolean $updated True if updated, false if inserted.
 * @param object  $post_before The post prior to this update, or null for new posts.
 */
function wpt_post_update_instant( $id, $post, $updated, $post_before ) {
	set_transient( '_wpt_twit_instant', $id, 10 );
	// future action has already run for this post.
	if ( get_transient( '_wpt_twit_future' ) && (int) get_transient( '_wpt_twit_future' ) === $id ) {
		delete_transient( '_wpt_twit_future' );

		return;
	}
	// xmlrpc action has already run for this post.
	if ( get_transient( '_wpt_twit_xmlrpc' ) && (int) get_transient( '_wpt_twit_xmlrpc' ) === $id ) {
		delete_transient( '_wpt_twit_xmlrpc' );

		return;
	}

	wpt_post_update( $id, 'instant', $post, $updated, $post_before );
}

/**
 * Status updates on XMLRPC posts.
 *
 * @param integer $id Post ID.
 *
 * @return post ID.
 */
function wpt_post_update_xmlrpc( $id ) {
	set_transient( '_wpt_twit_xmlrpc', $id, 10 );
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE || wp_is_post_revision( $id ) || ! wpt_in_post_type( $id ) ) {
		return $id;
	}
	wpt_mail( 'Status update sent on XMLRPC published post', $id );
	wpt_post_update( $id, 'xmlrpc' );
	return $id;
}

add_action( 'admin_notices', 'wpt_debugging_enabled', 10 );
/**
 * Show notice if X.com debugging is enabled.
 */
function wpt_debugging_enabled() {
	if ( current_user_can( 'manage_options' ) && WPT_DEBUG ) {
		echo "<div class='notice error important'><p>" . __( '<strong>XPoster</strong> debugging is enabled. Remember to disable debugging when you are finished.', 'wp-to-twitter' ) . '</p></div>';
	}
}

add_action( 'admin_notices', 'wpt_needs_bearer_token', 10 );
/**
 * Notify users if they need to add a bearer token for XPoster.
 */
function wpt_needs_bearer_token() {
	if ( current_user_can( 'manage_options' ) || current_user_can( 'wpt_twitter_oauth' ) ) {
		$screen = get_current_screen();
		if ( 'profile' === $screen->id && get_option( 'jd_individual_twitter_users' ) ) {
			$auth       = wp_get_current_user()->ID;
			$authorized = wtt_oauth_test( $auth );
			$bt         = get_user_meta( $auth, 'bearer_token', true );
		} else {
			$auth       = false;
			$authorized = wtt_oauth_test();
			$bt         = get_option( 'bearer_token', '' );
		}
		if ( ! $bt && $authorized ) {
			if ( $auth && get_option( 'jd_individual_twitter_users' ) ) {
				echo "<div class='notice error important'><p>" . __( '<strong>XPoster</strong> needs a Bearer Token added to your profile settings to support the X.com API.', 'wp-to-twitter' ) . '</p></div>';
			} elseif ( current_user_can( 'manage_options' ) ) {
				// Translators: URL to connection settings.
				echo "<div class='notice error important'><p>" . sprintf( __( '<strong>XPoster</strong> needs a Bearer Token added to the <a href="%s">connection settings</a> to support the X.com API.', 'wp-to-twitter' ), admin_url( 'admin.php?page=wp-tweets-pro&tab=connection' ) ) . '</p></div>';
			}
		}
	}
}

/**
 * Check connections.
 *
 * @param int|bool $auth User ID or false to check primary connection.
 * @param bool     $get_connections True to return an array with the valid connections. Default false.
 *
 * @return bool|array
 */
function wpt_check_connections( $auth = false, $get_connections = false ) {
	$connected = false;
	if ( ! $get_connections ) {
		$connected = ( wtt_oauth_test( $auth, 'verify' ) || wpt_mastodon_connection( $auth ) || wpt_bluesky_connection( $auth ) ) ? true : false;
	} else {
		$connected = array(
			'x'        => wtt_oauth_test( $auth, 'verify' ),
			'mastodon' => wpt_mastodon_connection( $auth ),
			'bluesky'  => wpt_bluesky_connection( $auth ),
		);
	}

	return $connected;
}

/**
 * Dismiss the missing connection notice.
 */
function wpt_dismiss_connection() {
	if ( isset( $_GET['page'] ) && 'wp-tweets-pro' === $_GET['page'] && isset( $_GET['dismiss'] ) && 'connection' === $_GET['dismiss'] ) {
		update_option( 'wpt_ignore_connection', 'true' );
	}
}
add_action( 'admin_init', 'wpt_dismiss_connection' );
/**
 * Display notices if update services are not connected.
 */
function wpt_needs_connection() {
	if ( isset( $_GET['page'] ) && 'wp-tweets-pro' === $_GET['page'] && ! 'true' === get_option( 'wpt_ignore_connection' ) ) {
		$message  = '';
		$mastodon = wpt_mastodon_connection();
		$x        = wpt_check_oauth();
		$bluesky  = wpt_bluesky_connection();
		// show notification to authenticate with Mastodon.
		if ( ! $mastodon ) {
			$admin_url = admin_url( 'admin.php?page=wp-tweets-pro&tab=mastodon' );
			// Translators: Settings page to authenticate Mastodon.
			$message .= '<li>' . sprintf( __( "Mastodon requires authentication. <a href='%s'>Update your Mastodon settings</a> to enable XPoster to send updates to Mastodon.", 'wp-to-twitter' ), $admin_url ) . '</li>';
		}
		// show notification to authenticate with OAuth.
		if ( ! $x ) {
			$admin_url = admin_url( 'admin.php?page=wp-tweets-pro' );
			// Translators: Settings page to authenticate X.com.
			$message .= '<li>' . sprintf( __( "X.com requires authentication by OAuth. <a href='%s'>Update your X settings</a> to enable XPoster to send updates to X.com.", 'wp-to-twitter' ), $admin_url ) . '</li>';
		}
		// show notification to authenticate with Bluesky.
		if ( ! $bluesky ) {
			$admin_url = admin_url( 'admin.php?page=wp-tweets-pro&tab=bluesky' );
			// Translators: Settings page to authenticate Bluesky.
			$message .= '<li>' . sprintf( __( "Bluesky requires authentication. <a href='%s'>Update your Bluesky settings</a> to enable XPoster to send updates to Bluesky.", 'wp-to-twitter' ), $admin_url ) . '</li>';
		}
		$message        = ( $message ) ? '<ul>' . $message . '</ul>' : '';
		$is_dismissible = '';
		$class          = 'xposter-connection';
		if ( $x || $mastodon || $bluesky ) {
			$class          = 'xposter-connection dismissible';
			$dismiss_url    = add_query_arg( 'dismiss', 'connection', admin_url( 'admin.php?page=wp-tweets-pro' ) );
			$is_dismissible = ' <a href="' . esc_url( $dismiss_url ) . '" class="button button-secondary">' . __( 'Ignore', 'wp-to-twitter' ) . '</a>';
		}
		if ( $message ) {
			echo "<div class='notice notice-error $class'>$message $is_dismissible</div>";
		}
	}
}
add_action( 'admin_notices', 'wpt_needs_connection' );