diff --git a/assets/css/admin-autoshare-for-twitter.css b/assets/css/admin-autoshare-for-twitter.css index a5a4ab1f..4b4f372d 100644 --- a/assets/css/admin-autoshare-for-twitter.css +++ b/assets/css/admin-autoshare-for-twitter.css @@ -265,3 +265,87 @@ tbody .autoshare-for-twitter-status-logo--disabled::before { margin-top: 12px; display: block; } + +#autopost_for_x_rate_monitor_dashboard_widget .inside { + padding: 0; + margin-top: 0px; +} + +.autoshare-for-twitter-no-accounts { + padding: 0px 12px 12px 12px; +} + +.autoshare-for-twitter-rate-monitor__users { + border-top: 1px solid #f0f0f1; +} + +.autoshare-for-twitter-rate-monitor__user { + align-items: center; + border-bottom: 1px solid #f0f0f1; + display: flex; + padding: 12px; +} + +.autoshare-for-twitter-rate-monitor__user img { + border-radius: 50%; + margin-right: 10px; + max-width: 48px; +} + +#autopost_for_x_rate_monitor_dashboard_widget .autoshare-for-twitter-rate-monitor__user h3 { + font-weight: bold; + margin-bottom: 0; + margin-right: 10px; +} + +.autoshare-for-twitter-rate-monitor__user-info p, +.autoshare-for-twitter-rate-monitor__rate p { + margin-bottom: 0; + margin-top: 0; +} + +.autoshare-for-twitter-rate-monitor__rate-reset { + font-size: 12px; + font-style: italic; +} + +.autoshare-for-twitter-rate-monitor__app { + align-items: center; + display: flex; + padding: 12px; +} + +.autoshare-for-twitter-rate-monitor__disclaimer { + background: #f6f7f7; + color: #50575e; + padding: 12px; +} + +.autoshare-for-twitter-rate-monitor__disclaimer ul { + margin-bottom: 0; + margin-top: 0; +} + +.autoshare-for-twitter-rate-monitor__disclaimer p { + margin-bottom: 8px; + margin-top: 0; +} + +.autoshare-for-twitter-editor-panel .autoshare-for-twitter-rate-monitor__user { + border-bottom: 0; + margin-bottom: 16px; + padding: 0; +} + +.autoshare-for-twitter-editor-panel .autoshare-for-twitter-rate-monitor__app { + padding: 0; + margin-bottom: 16px; +} + +.autoshare-for-twitter-editor-panel .autoshare-for-twitter-rate-monitor__disclaimer { + margin-bottom: 16px; +} + +.autoshare-for-twitter-editor-panel .autoshare-for-twitter-rate-monitor__disclaimer p { + margin-bottom: 0; +} \ No newline at end of file diff --git a/includes/core.php b/includes/core.php index bbf95d77..9bf4f2cd 100644 --- a/includes/core.php +++ b/includes/core.php @@ -51,6 +51,8 @@ function setup() { add_filter( 'autoshare_for_twitter_attached_image', __NAMESPACE__ . '\maybe_disable_upload_image', 10, 2 ); add_action( 'admin_init', __NAMESPACE__ . '\handle_notice_dismiss' ); add_action( 'admin_notices', __NAMESPACE__ . '\migrate_to_twitter_v2_api' ); + add_action( 'autoshare_for_twitter_after_status_update', __NAMESPACE__ . '\update_account_rate_limits', 10, 5 ); + add_action( 'wp_dashboard_setup', __NAMESPACE__ . '\register_rate_monitor_dashboard_widget' ); } /** @@ -222,3 +224,305 @@ function handle_notice_dismiss() { update_option( 'autoshare_migrate_to_v2_api_notice_dismissed', true ); } } + +/** + * Update the account rate limits from the last X/Twitter API request. + * + * @param object $response The response from the X/Twitter endpoint. + * @param array $update_data Data to send to the X/Twitter endpoint. + * @param \WP_Post $post The post associated with the tweet. + * @param string $account_id The account ID associated with the tweet. + * @param array|null $last_headers The headers from the last request. + * @return void + */ +function update_account_rate_limits( $response, $update_data, $post, $account_id, $last_headers ) { + + if ( empty( $account_id ) ) { + return; + } + + $accounts = get_option( 'autoshare_for_twitter_accounts', array() ); + + if ( empty( $accounts[ $account_id ] ) ) { + return; + } + + $rate_limits = parse_last_headers( + $last_headers, + array( + 'rate_limit_limit' => 'x_rate_limit_limit', + 'rate_limit_reset' => 'x_rate_limit_reset', + 'rate_limit_remaining' => 'x_rate_limit_remaining', + ) + ); + + $app_rate_limits = parse_last_headers( + $last_headers, + array( + 'app_limit_24hour_limit' => 'x_app_limit_24hour_limit', + 'app_limit_24hour_reset' => 'x_app_limit_24hour_reset', + 'app_limit_24hour_remaining' => 'x_app_limit_24hour_remaining', + ) + ); + + $user_rate_limits = parse_last_headers( + $last_headers, + array( + 'user_limit_24hour_limit' => 'x_user_limit_24hour_limit', + 'user_limit_24hour_reset' => 'x_user_limit_24hour_reset', + 'user_limit_24hour_remaining' => 'x_user_limit_24hour_remaining', + ) + ); + + foreach ( $accounts as $key => $account ) { + $current_rate_limits = ( isset( $account['rate_limits'] ) && is_array( $account['rate_limits'] ) ) ? $account['rate_limits'] : array(); + + // Update the "global" and app rate limits on all accounts. + $account_rate_limits = array_merge( $rate_limits, $app_rate_limits ); + + // Update the user rate limits on the account that made the request. + if ( $account['id'] === $account_id ) { + $account_rate_limits = array_merge( $user_rate_limits, $account_rate_limits ); + } + + // Merge the current rate limits with the new rate limits. + $account_rate_limits = array_merge( $current_rate_limits, $account_rate_limits ); + + $accounts[ $key ]['rate_limits'] = $account_rate_limits; + } + + update_option( 'autoshare_for_twitter_accounts', $accounts ); +} + +/** + * Register the Rate Monitor dashboard widget. + * + * @return void + */ +function register_rate_monitor_dashboard_widget() { + + wp_add_dashboard_widget( + 'autopost_for_x_rate_monitor_dashboard_widget', + esc_html__( 'Autopost for X — Rate Monitor', 'autoshare-for-twitter' ), + __NAMESPACE__ . '\display_rate_monitor_dashboard_widget' + ); +} + +/** + * Display the Rate Monitor dashboard widget. + * + * @return void + */ +function display_rate_monitor_dashboard_widget() { + $accounts = get_option( 'autoshare_for_twitter_accounts', array() ); + + if ( empty( $accounts ) ) { + printf( + '
', + esc_html__( 'No X/Twitter accounts are connected. Please connect at least one X/Twitter account to continue using Autopost for X.', 'autoshare-for-twitter' ) + ); + return; + } + + $app_rate_limits_markup = ''; + $users_rate_limits_markup = ''; + + foreach ( $accounts as $account ) { + + $account_markup = ''; + + if ( ! empty( $account['rate_limits'] ) ) { + $account_markup = get_user_rate_limits_markup( $account['rate_limits'] ); + + if ( empty( $app_rate_limits_markup ) ) { // We only need to display the app rate limits once. + $app_rate_limits_markup = get_app_rate_limits_markup( $account['rate_limits'] ); + } + } else { + $account_markup = sprintf( + '%s
', + esc_html__( 'No X/Twitter rate limit available yet. Make a post to X/Twitter first.', 'autoshare-for-twitter' ) + ); + } + + $users_rate_limits_markup .= sprintf( + ' ', + esc_url( $account['profile_image_url'] ), + esc_attr( $account['name'] ), + esc_html( $account['username'] ), + $account_markup // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + ); + } + + $footnotes = array( + __( 'User 24-Hour Limit: The maximum number of requests a single user can make across all API endpoints within a 24-hour period.', 'autoshare-for-twitter' ), + __( 'App 24-Hour Limit: The total number of API calls your app can make across all users within a 24-hour period.', 'autoshare-for-twitter' ), + ); + + $footnotes = array_map( + function ( $footnote ) { + return sprintf( + '%s
', + esc_html__( 'No X/Twitter rate limit available yet. Make a post to X/Twitter first.', 'autoshare-for-twitter' ) + ); + } + + return get_rate_limits_markup( + __( 'User 24-Hour Limit:', 'autoshare-for-twitter' ), + $remaining, + $limit, + $reset + ); +} + +/** + * Get app rate limits markup. + * + * @param array $rate_limits Rate limits. + * @return string + */ +function get_app_rate_limits_markup( $rate_limits ) { + + $remaining = isset( $rate_limits['app_limit_24hour_remaining'] ) ? $rate_limits['app_limit_24hour_remaining'] : ''; + $limit = isset( $rate_limits['app_limit_24hour_limit'] ) ? $rate_limits['app_limit_24hour_limit'] : ''; + $reset = isset( $rate_limits['app_limit_24hour_reset'] ) ? $rate_limits['app_limit_24hour_reset'] : ''; + + return get_rate_limits_markup( + __( 'App 24-Hour Limit:', 'autoshare-for-twitter' ), + $remaining, + $limit, + $reset + ); +} + +/** + * Get rate limits markup. + * + * @param string $title Rate limit title. + * @param int $remaining Remaining rate limit. + * @param int $limit Total rate limit. + * @param int $reset Rate limit reset time. + * @return string + */ +function get_rate_limits_markup( $title, $remaining, $limit, $reset ) { + + $remaining = isset( $remaining ) ? (int) $remaining : esc_html__( 'N/A', 'autoshare-for-twitter' ); + $limit = isset( $limit ) ? (int) $limit : esc_html__( 'N/A', 'autoshare-for-twitter' ); + $reset = isset( $reset ) ? human_readable_time( $reset ) : esc_html__( 'N/A', 'autoshare-for-twitter' ); + + return sprintf( + ' ', + esc_html( $title ), + sprintf( + /* translators: %1$s: Remaining, %2$s: Limit */ + esc_html__( '%1$s of %2$s', 'autoshare-for-twitter' ), + esc_html( $remaining ), + esc_html( $limit ) + ), + sprintf( + /* translators: %1$s: Reset time */ + esc_html__( 'Resets on %1$s', 'autoshare-for-twitter' ), + esc_html( $reset ) + ) + ); +} diff --git a/src/js/components/TwitterAccounts.js b/src/js/components/TwitterAccounts.js index 79add787..8b77f9ac 100644 --- a/src/js/components/TwitterAccounts.js +++ b/src/js/components/TwitterAccounts.js @@ -1,8 +1,10 @@ -import { ToggleControl, ExternalLink } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { ToggleControl, ExternalLink, Tooltip } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { dateI18n, getSettings } from '@wordpress/date'; import { useTweetAccounts } from '../hooks'; const { connectedAccounts, connectAccountUrl } = adminAutoshareForTwitter; +const settings = getSettings(); /** * Twitter accounts component. @@ -13,12 +15,32 @@ export function TwitterAccounts() { const accounts = connectedAccounts ? Object.values( connectedAccounts ) : []; + const [ firstAccount ] = accounts; + + const [ tweetAccounts ] = useTweetAccounts(); return (+ + { __( 'Note:', 'autoshare-for-twitter' ) } + { ' ' } + { __( + 'The displayed API rate limits are updated only when a tweet is posted. Since there is no dedicated endpoint for real-time usage data, the information provided may not fully reflect the current API usage, especially if other tweets are made through the same app.', + 'autoshare-for-twitter' + ) } +
++ { __( + 'No X/Twitter rate limit available yet. Make a post to X/Twitter first.', + 'autoshare-for-twitter' + ) } +
+ ); + } + + return ( +
+
+ { formattedResetTime } +
+