diff --git a/.env.example b/.env.example index 9372ce9..2b9577d 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,7 @@ API_KEY=your_api_key API_SECRET_KEY=your_api_secret_key ACCESS_TOKEN=your_access_token ACCESS_TOKEN_SECRET=your_access_token_secret +TWITTER_BACKEND=twitter +XQUIK_API_KEY=your_xquik_api_key +XQUIK_ACCOUNT=your_x_account_username +XQUIK_API_BASE=https://xquik.com/api/v1 diff --git a/README.md b/README.md index 7c867e6..0d07005 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,15 @@ composer install ``` ### Configuration -#### Option 1: Using Environment Variables +#### Option 1: Using Shell Environment Variables -Rename the file .env.example to .env and update it with your Twitter API credentials: +Export your credentials before running the script: -```dotenv -API_KEY=your_api_key -API_SECRET_KEY=your_api_secret_key -ACCESS_TOKEN=your_access_token -ACCESS_TOKEN_SECRET=your_access_token_secret +```bash +export API_KEY=your_api_key +export API_SECRET_KEY=your_api_secret_key +export ACCESS_TOKEN=your_access_token +export ACCESS_TOKEN_SECRET=your_access_token_secret ``` #### Option 2: Using config.php @@ -47,11 +47,35 @@ define('API_KEY', 'your_api_key'); define('API_SECRET_KEY', 'your_api_secret_key'); define('ACCESS_TOKEN', 'your_access_token'); define('ACCESS_TOKEN_SECRET', 'your_access_token_secret'); +define('TWITTER_BACKEND', 'twitter'); +define('XQUIK_API_KEY', 'your_xquik_api_key'); +define('XQUIK_ACCOUNT', 'your_x_account_username'); +define('XQUIK_API_BASE', 'https://xquik.com/api/v1'); ?> ``` +Environment variables override values from `config.php`. + Note: For security reasons, ensure that config.php and .env are added to your .gitignore file and not committed to version control. +### Optional: Posting with Xquik + +Set `TWITTER_BACKEND` to `xquik` to post through Xquik: + +```dotenv +TWITTER_BACKEND=xquik +XQUIK_API_KEY=your_xquik_api_key +XQUIK_ACCOUNT=your_x_account_username +XQUIK_API_BASE=https://xquik.com/api/v1 +``` + +The script sends `account` and `text` to `POST /api/v1/x/tweets` with the +`x-api-key` header. The default `twitter` backend continues to use X API v2 +with OAuth 1.0a. + +Xquik is an independent third-party service. Not affiliated with X Corp. +"Twitter" and "X" are trademarks of X Corp. + ### Customize Your Tweet Edit the post_tweet.php script to customize the tweet content: diff --git a/config.php b/config.php index 866b4ec..6b5466a 100644 --- a/config.php +++ b/config.php @@ -3,4 +3,8 @@ define('API_SECRET_KEY', 'YOUR_API_SECRET_KEY'); define('ACCESS_TOKEN', 'YOUR_ACCESS_TOKEN'); define('ACCESS_TOKEN_SECRET', 'YOUR_ACCESS_TOKEN_SECRET'); +define('TWITTER_BACKEND', 'twitter'); +define('XQUIK_API_KEY', 'YOUR_XQUIK_API_KEY'); +define('XQUIK_ACCOUNT', 'YOUR_X_ACCOUNT_USERNAME'); +define('XQUIK_API_BASE', 'https://xquik.com/api/v1'); ?> diff --git a/post_tweet.php b/post_tweet.php index ef67e0a..de9976d 100644 --- a/post_tweet.php +++ b/post_tweet.php @@ -6,25 +6,24 @@ error_reporting(E_ALL); ini_set('display_errors', getenv('APP_DEBUG') ? '1' : '0'); -// Twitter API credentials -if (!file_exists(__DIR__ . '/config.php')) { - fwrite(STDERR, "Error: config.php not found. Copy .env.example values into config.php.\n"); +// Optional file-based credentials. Environment variables override these values. +if (file_exists(__DIR__ . '/config.php')) { + require_once __DIR__ . '/config.php'; +} + +$backend = strtolower(trim(getConfigValue('TWITTER_BACKEND', 'twitter'))); +if ($backend !== 'twitter' && $backend !== 'xquik') { + fwrite(STDERR, "Error: TWITTER_BACKEND must be twitter or xquik.\n"); exit(1); } -require_once __DIR__ . '/config.php'; - -// Validate that credentials have been configured -$credentials = [ - 'API_KEY' => API_KEY, - 'API_SECRET_KEY' => API_SECRET_KEY, - 'ACCESS_TOKEN' => ACCESS_TOKEN, - 'ACCESS_TOKEN_SECRET' => ACCESS_TOKEN_SECRET, -]; -foreach ($credentials as $name => $value) { - if (empty($value) || strpos($value, 'YOUR_') === 0) { - echo "Error: $name is not configured. Please update config.php with your actual credentials.\n"; - exit(1); - } + +if ($backend === 'twitter') { + validateCredentials([ + 'API_KEY' => getConfigValue('API_KEY'), + 'API_SECRET_KEY' => getConfigValue('API_SECRET_KEY'), + 'ACCESS_TOKEN' => getConfigValue('ACCESS_TOKEN'), + 'ACCESS_TOKEN_SECRET' => getConfigValue('ACCESS_TOKEN_SECRET'), + ]); } // The tweet content variables @@ -54,16 +53,21 @@ */ } +if ($backend === 'xquik') { + postTweetWithXquik($fullTweet); + exit(0); +} + // Twitter API endpoint for creating a tweet (API v2) $url = 'https://api.twitter.com/2/tweets'; // OAuth parameters $oauth = [ - 'oauth_consumer_key' => API_KEY, + 'oauth_consumer_key' => getConfigValue('API_KEY'), 'oauth_nonce' => bin2hex(random_bytes(16)), 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_timestamp' => time(), - 'oauth_token' => ACCESS_TOKEN, + 'oauth_token' => getConfigValue('ACCESS_TOKEN'), 'oauth_version' => '1.0', ]; @@ -79,7 +83,10 @@ $base_info = buildBaseString($url, 'POST', $signature_params); // Generate the composite signing key -$composite_key = rawurlencode(API_SECRET_KEY) . '&' . rawurlencode(ACCESS_TOKEN_SECRET); +$composite_key = + rawurlencode(getConfigValue('API_SECRET_KEY')) . + '&' . + rawurlencode(getConfigValue('ACCESS_TOKEN_SECRET')); // Generate the OAuth signature and add it to the OAuth parameters $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); @@ -107,8 +114,10 @@ $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_err = $response === false ? curl_error($ch) : null; -// Close cURL -curl_close($ch); +// PHP 8 releases cURL handles automatically. PHP 8.5 deprecates curl_close(). +if (PHP_VERSION_ID < 80000) { + curl_close($ch); +} if ($response === false) { $error_message = "Failed to post tweet. cURL error: $curl_err\n"; @@ -148,6 +157,113 @@ echo $error_message; logTwitterResponse($http_status, $error_message); + exit(1); +} + +/** + * Reads a config value from the environment or config.php. + * + * @param string $name + * @param string $default + * @return string + */ +function getConfigValue($name, $default = '') +{ + $environmentValue = getenv($name); + if ($environmentValue !== false && $environmentValue !== '') { + return $environmentValue; + } + + return defined($name) ? (string) constant($name) : $default; +} + +/** + * Validates required credentials. + * + * @param array $credentials + * @return void + */ +function validateCredentials($credentials) +{ + foreach ($credentials as $name => $value) { + if (empty($value) || stripos($value, 'your_') === 0) { + fwrite(STDERR, "Error: $name is not configured.\n"); + exit(1); + } + } +} + +/** + * Posts a tweet through Xquik's REST API. + * + * @param string $fullTweet + * @return void + */ +function postTweetWithXquik($fullTweet) +{ + $apiKey = getConfigValue('XQUIK_API_KEY'); + $account = getConfigValue('XQUIK_ACCOUNT'); + validateCredentials([ + 'XQUIK_API_KEY' => $apiKey, + 'XQUIK_ACCOUNT' => $account, + ]); + + $apiBase = rtrim(getConfigValue('XQUIK_API_BASE', 'https://xquik.com/api/v1'), '/'); + $postfields = json_encode([ + 'account' => $account, + 'text' => $fullTweet, + ], JSON_UNESCAPED_UNICODE); + if ($postfields === false) { + fwrite(STDERR, "Error: Could not encode the Xquik request.\n"); + exit(1); + } + + $ch = curl_init(); + if ($ch === false) { + fwrite(STDERR, "Error: Could not initialize cURL.\n"); + exit(1); + } + + curl_setopt_array($ch, [ + CURLOPT_URL => $apiBase . '/x/tweets', + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'x-api-key: ' . $apiKey, + ], + CURLOPT_POSTFIELDS => $postfields, + CURLOPT_RETURNTRANSFER => true, + ]); + + $response = curl_exec($ch); + $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curl_err = $response === false ? curl_error($ch) : null; + if (PHP_VERSION_ID < 80000) { + curl_close($ch); + } + + if ($response === false) { + $error_message = "Failed to post tweet with Xquik. cURL error: $curl_err\n"; + fwrite(STDERR, $error_message); + logTwitterResponse($http_status, $error_message); + exit(1); + } + + $response_data = json_decode($response, true); + if ($http_status >= 200 && $http_status < 300 && isset($response_data['tweetId'])) { + $success_message = "Tweet posted successfully with Xquik! Tweet ID: " . $response_data['tweetId']; + echo $success_message; + logTwitterResponse($http_status, $success_message); + return; + } + + $error_message = "Failed to post tweet with Xquik. HTTP Status Code: $http_status\n"; + if (is_array($response_data) && isset($response_data['message'])) { + $error_message .= "Message: " . $response_data['message'] . "\n"; + } + fwrite(STDERR, $error_message); + logTwitterResponse($http_status, $error_message); + exit(1); } /**