public/Invoke-XPost.ps1

function Invoke-XPost {
    <#
        .EXAMPLE
            Invoke-XPost -ConsumerKey $ConsumerKey -ConsumerSecret $ConsumerSecret -Token $Token -TokenSecret $TokenSecret -Text $Text
     
    #>

    [CmdletBinding()]
    param (

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$ConsumerKey,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$ConsumerSecret,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Text,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Token,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$TokenSecret

    )
    process{

        $ErrorActionPreference = 'Stop'

        try {
            
            $oauth_consumer_key = $ConsumerKey  # API Key (Consumer Key)
            $oauth_consumer_secret = $ConsumerSecret  # API Key Secret (Consumer Secret)
            $oauth_token = $Token  # Access Token
            $oauth_token_secret = $TokenSecret  # Access Token Secret

            # Enable modern TLS protocols to avoid connection failures
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13

            $oauth_nonce = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes([System.DateTime]::Now.Ticks.ToString()))
            $ts = [System.DateTime]::UtcNow - [System.DateTime]::UnixEpoch
            $oauth_timestamp = [System.Convert]::ToInt64($ts.TotalSeconds).ToString()

            # Build the signature base string (no body params for JSON POST)
            $signature_base = 'POST&' + [System.Uri]::EscapeDataString('https://api.twitter.com/2/tweets') + '&'
            $signature_base += [System.Uri]::EscapeDataString("oauth_consumer_key=$oauth_consumer_key&")
            $signature_base += [System.Uri]::EscapeDataString("oauth_nonce=$oauth_nonce&")
            $signature_base += [System.Uri]::EscapeDataString("oauth_signature_method=HMAC-SHA1&")
            $signature_base += [System.Uri]::EscapeDataString("oauth_timestamp=$oauth_timestamp&")
            $signature_base += [System.Uri]::EscapeDataString("oauth_token=$oauth_token&")
            $signature_base += [System.Uri]::EscapeDataString("oauth_version=1.0")

            $signature_key = [System.Uri]::EscapeDataString($oauth_consumer_secret) + '&' + [System.Uri]::EscapeDataString($oauth_token_secret)

            $hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1
            $hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($signature_key)
            $oauth_signature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($signature_base)))

            # Build the Authorization header
            $oauth_authorization = 'OAuth '
            $oauth_authorization += 'oauth_consumer_key="' + [System.Uri]::EscapeDataString($oauth_consumer_key) + '",'
            $oauth_authorization += 'oauth_nonce="' + [System.Uri]::EscapeDataString($oauth_nonce) + '",'
            $oauth_authorization += 'oauth_signature="' + [System.Uri]::EscapeDataString($oauth_signature) + '",'
            $oauth_authorization += 'oauth_signature_method="HMAC-SHA1",'
            $oauth_authorization += 'oauth_timestamp="' + [System.Uri]::EscapeDataString($oauth_timestamp) + '",'
            $oauth_authorization += 'oauth_token="' + [System.Uri]::EscapeDataString($oauth_token) + '",'
            $oauth_authorization += 'oauth_version="1.0"'

            # Prepare JSON body
            $bodyJson = @{ text = $Text } | ConvertTo-Json -Compress
            $post_body = [System.Text.Encoding]::UTF8.GetBytes($bodyJson)

            # Send the request
            [System.Net.HttpWebRequest] $request = [System.Net.WebRequest]::Create('https://api.twitter.com/2/tweets')
            $request.Method = 'POST'
            $request.Headers.Add('Authorization', $oauth_authorization)
            $request.ContentType = 'application/json'
            $request.ContentLength = $post_body.Length
            $bodyStream = $request.GetRequestStream()
            $bodyStream.Write($post_body, 0, $post_body.Length)
            $bodyStream.Flush()
            $bodyStream.Close()

            $response = $request.GetResponse()
            
            # Get rate limit headers
            $rateLimit = $response.Headers["x-rate-limit-limit"]
            $rateRemaining = $response.Headers["x-rate-limit-remaining"]
            $rateReset = $response.Headers["x-rate-limit-reset"]
            $resetTime = if ($rateReset) { [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddSeconds($rateReset)) } else { "N/A" }
            
            # Read response body (for success details, e.g., tweet ID)
            $stream = New-Object System.IO.StreamReader($response.GetResponseStream())
            $body = $stream.ReadToEnd()
            $stream.Close()
            
            Write-Output "Post successful! Status: $($response.StatusCode)"
            Write-Output "Response Body: $body"
            Write-Output "Rate Limits: Limit=$rateLimit, Remaining=$rateRemaining, Resets At=$resetTime (local time)"
            
            $response.Close()

        } catch {

            $err = $_

            # Unwrap nested exceptions to find the innermost one (likely WebException)
            $ex = $err.Exception

            while ($ex.InnerException) {

                $ex = $ex.InnerException
            }

            $errResp = $ex.Response
            
            Write-Output "Exception Type: $($err.Exception.GetType().FullName)"
            Write-Output "Exception Message: $($err.Exception.Message)"

            if ($err.Exception.InnerException) {

                Write-Output "Inner Exception: $($err.Exception.InnerException.Message)"
            }
            
            if ($errResp) {

                # Get rate limit headers from error response (if present)
                $rateLimit = $errResp.Headers["x-rate-limit-limit"]
                $rateRemaining = $errResp.Headers["x-rate-limit-remaining"]
                $rateReset = $errResp.Headers["x-rate-limit-reset"]
                $resetTime = if ($rateReset) { [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddSeconds($rateReset)) } else { "N/A" }
                
                # Read error response body (e.g., for JSON error details)
                $errStream = New-Object System.IO.StreamReader($errResp.GetResponseStream())
                $errBody = $errStream.ReadToEnd()
                $errStream.Close()
                
                Write-Output "Post failed! HTTP Status: $($errResp.StatusCode) $($errResp.StatusDescription)"
                Write-Output "Error Body: $errBody"
                Write-Output "Rate Limits: Limit=$rateLimit, Remaining=$rateRemaining, Resets At=$resetTime (local time)"

            } else {

                Write-Output "Post failed! Non-HTTP error (e.g., connection issue)."

            } # if

        } # try catch

    } # process

} # function