BluebirdPS.psm1




using namespace System.Collections
using namespace System.Collections.Generic
using namespace Collections.ObjectModel
using namespace System.Management.Automation
using namespace System.Diagnostics.CodeAnalysis
using namespace Microsoft.PowerShell.Commands
using namespace BluebirdPS
using namespace BluebirdPS.APIV2
using namespace BluebirdPS.APIV1

# --------------------------------------------------------------------------------------------------

#region set base path variables
if ($IsWindows) {
    $DefaultSavePath = Join-Path -Path $env:USERPROFILE -ChildPath '.BluebirdPS'
} else {
    $DefaultSavePath = Join-Path -Path $env:HOME -ChildPath '.BluebirdPS'
}
#endregion

#region Authentication variables and setup
[SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$OAuth =  @{
    ApiKey = $null
    ApiSecret = $null
    AccessToken = $null
    AccessTokenSecret = $null
    BearerToken = $null
}
#endregion

#region BluebirdPS configuration variable
[SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$BluebirdPSConfiguration = [Configuration]@{
    ConfigurationPath = Join-Path -Path $DefaultSavePath -ChildPath 'Configuration.json'
    CredentialsPath = Join-Path -Path $DefaultSavePath -ChildPath 'twittercred.sav'
}
#endregion

#region other variables
[SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$BluebirdPSHistoryList = [List[ResponseData]]::new()
#endregion


function Get-ErrorCategory {
    [CmdletBinding(DefaultParameterSetName = 'APIV1.1')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'APIV1.1')]
        [string]$StatusCode,
        [Parameter(Mandatory, ParameterSetName = 'APIV1.1')]
        [string]$ErrorCode,

        [Parameter(Mandatory, ParameterSetName = 'APIV2')]
        [string]$ErrorType
    )

    if ($PSCmdlet.ParameterSetName -eq 'APIV2') {
        switch ($ErrorType) {
            'about:blank'                                                    { return 'NotSpecified' }
            'https://api.twitter.com/2/problems/not-authorized-for-resource' { return 'PermissionDenied' }
            'https://api.twitter.com/2/problems/not-authorized-for-field'    { return 'PermissionDenied' }
            'https://api.twitter.com/2/problems/invalid-request'             { return 'InvalidArgument' }
            'https://api.twitter.com/2/problems/client-forbidden'            { return 'PermissionDenied' }
            'https://api.twitter.com/2/problems/disallowed-resource'         { return 'PermissionDenied' }
            'https://api.twitter.com/2/problems/unsupported-authentication'  { return 'AuthenticationError' }
            'https://api.twitter.com/2/problems/usage-capped'                { return 'QuotaExceeded' }
            'https://api.twitter.com/2/problems/streaming-connection'        { return 'ConnectionError' }
            'https://api.twitter.com/2/problems/client-disconnected'         { return 'ConnectionError' }
            'https://api.twitter.com/2/problems/operational-disconnect'      { return 'ResourceUnavailable' }
            'https://api.twitter.com/2/problems/rule-cap'                    { return 'QuotaExceeded' }
            'https://api.twitter.com/2/problems/invalid-rules'               { return 'InvalidArgument' }
            'https://api.twitter.com/2/problems/duplicate-rules'             { return 'InvalidOperation' }
            'https://api.twitter.com/2/problems/resource-not-found'          { return 'ObjectNotFound' }
        }

    } else {
        switch ($StatusCode) {
            400 {
                switch ($ErrorCode) {
                    324                    { return 'OperationStopped' }
                    325                    { return 'ObjectNotFound' }
                    { $_ -in 323, 110 }    { return 'InvalidOperation' }
                    215                    { return 'AuthenticationError' }
                    { $_ -in 3, 7, 8, 44 } { return 'InvalidArgument' }
                    407                    { return 'ResourceUnavailable' }
                }
            }
            401 {
                if ($ErrorCode -in 417, 135, 32, 416) {
                    return 'InvalidOperation'
                }
            }
            403 {
                switch ($ErrorCode) {
                    326 { return 'SecurityError' }
                    { $_ -in 200, 272, 160, 203, 431 }  { return 'InvalidOperation' }
                    { $_ -in 386, 205, 226, 327 }       { return 'QuotaExceeded' }
                    { $_ -in 99, 89 }                   { return 'AuthenticationError' }
                    { $_ -in 195, 92 }                  { return 'ConnectionError' }
                    { $_ -in 354, 186, 38, 120, 163 }   { return 'InvalidArgument' }
                    { $_ -in 214, 220, 261, 187, 349,
                        385, 415, 271, 185, 36, 63, 64,
                        87, 179, 93, 433, 139, 150, 151,
                        161, 425 }                       { return 'PermissionDenied' }
                }
            }
            404 {
                if ($ErrorCode -in 34, 108, 109, 422, 421, 13, 17, 144, 34, 50) {
                    return 'InvalidOperation'
                } elseif ($ErrorCode -eq 25) {
                    return 'InvalidArgument'
                }
            }
            406 {
                return 'InvalidData'
            }
            409 {
                if ($ErrorCode -eq 355) {
                    return 'InvalidOperation'
                }
            }
            410 {
                if ($ErrorCode -eq 68) {
                    return 'ConnectionError'
                } elseif ($ErrorCode -eq 251) {
                    return 'NotImplemented'
                }
            }
            415 {
                return 'LimitsExceeded'
            }
            420 {
                return 'QuotaExceeded'
            }
            422 {
                if ($ErrorCode -eq 404) {
                    return 'InvalidOperation'
                } else {
                    return 'InvalidArgument'
                }
            }
            429 {
                if ($ErrorCode -eq 88) {
                    return 'QuotaExceeded'
                }
            }
            500 {
                if ($ErrorCode -eq 131) {
                    return 'ResourceUnavailable'
                }
            }
            503 {
                if ($ErrorCode -eq 130) {
                    return 'ResourceBusy'
                }
            }
        }
    }

    return 'NotSpecified'
}


function Get-ExceptionType {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ErrorCategory
    )

    switch ($ErrorCategory) {
        'AuthenticationError'                           { return 'AuthenticationException' }
        {$_ -in 'InvalidOperation','OperationStopped',
        'NotImplemented' }                              { return 'InvalidOperationException' }
        {$_ -in 'InvalidArgument','InvalidData' }       { return 'InvalidArgumentException' }
        {$_ -in 'LimitsExceeded','QuotaExceeded' }      { return 'LimitsExceededException' }
        {$_ -in 'PermissionDenied','ResourceBusy',
        'ResourceUnavailable' }                         { return 'ResourceViolationException' }
        'ObjectNotFound'                                { return 'ResourceNotFoundException' }
        'SecurityError'                                 { return 'SecurityException' }
        'ConnectionError'                               { return 'ConnectionException' }
        default                                         { return 'UnspecifiedException'}
    }

}


function Get-SendMediaStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Alias('media_id')]
        [string]$MediaId,

        [ValidateRange(1,[int]::MaxValue)]
        [int]$WaitSeconds
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://upload.twitter.com/1.1/media/upload.json'
        Query = @{'command' = 'STATUS'; 'media_id' = $MediaId }
    }

    if ($PSBoundParameters.ContainsKey('WaitSeconds')) {
        $StatusCheck = 0
        do {

            $StatusCheck++
            $Activity = 'Waiting {0} seconds before refreshing upload status for media id {1}' -f $WaitSeconds, $MediaId
            $CurrentOperation = 'Check status #{0}' -f $StatusCheck
            $Status = 'Total seconds waited {0}' -f $TotalWaitSeconds
            Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status

            Start-Sleep -Seconds $WaitSeconds
            $TotalWaitSeconds += $WaitSeconds

            $SendMediaStatus = Invoke-TwitterRequest -RequestParameters $Request
            if ($SendMediaStatus -is [ErrorRecord]) {
                $PSCmdlet.ThrowTerminatingError($SendMediaStatus)
            }

            if ($SendMediaStatus.'processing_info'.'error') {
                $SendMediaStatus.'processing_info'.'error' | Write-Error -ErrorAction Stop
            }
            if ($SendMediaStatus.'processing_info'.'check_after_secs') {
                $WaitSeconds = $SendMediaStatus.'processing_info'.'check_after_secs' -as [int]
            }

        } while ($SendMediaStatus.'processing_info'.'state' -eq 'in_progress')
        Write-Progress -Activity "Media upload status check completed" -Completed

        $SendMediaStatus

    } else {
        Invoke-TwitterRequest -RequestParameters $Request
    }

}


function Get-TwitterException {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ExceptionType,
        [Parameter(Mandatory)]
        [string]$ErrorMessage
    )
    switch ($ExceptionType) {
        AuthenticationException    { return [AuthenticationException]::new($ErrorMessage) }
        InvalidOperationException  { return [InvalidOperationException]::new($ErrorMessage) }
        InvalidArgumentException   { return [InvalidArgumentException]::new($ErrorMessage) }
        LimitsExceededException    { return [LimitsExceededException]::new($ErrorMessage) }
        ResourceViolationException { return [ResourceViolationException]::new($ErrorMessage) }
        ResourceNotFoundException  { return [ResourceNotFoundException]::new($ErrorMessage) }
        SecurityException          { return [SecurityException]::new($ErrorMessage) }
        ConnectionException        { return [ConnectionException]::new($ErrorMessage) }
        UnspecifiedException       { return [UnspecifiedException]::new($ErrorMessage) }
        default                    { return [UnspecifiedException]::new($ErrorMessage) }
    }
}


function Invoke-TwitterVerifyCredentials {
    [CmdletBinding()]
    param(
        [switch]$BearerToken
    )

    if ($BearerToken.IsPresent) {
        $Request = [TwitterRequest]@{
            OAuthVersion = 'OAuth2Bearer'
            Endpoint = 'https://api.twitter.com/2/users/{0}' -f $BluebirdPSConfiguration.AuthUserId
        }
    } else {
        $Request = [TwitterRequest]@{
            Endpoint = 'https://api.twitter.com/1.1/account/verify_credentials.json'
            Query = @{ include_entities = 'false'; skip_status = 'true' }
        }
    }

    $Request.SetCommandName((Get-PSCallStack).Command[1])

    try {
        Invoke-TwitterRequest -RequestParameters $Request
        $BluebirdPSConfiguration.AuthValidationDate = Get-Date
    }
    catch {
        $BluebirdPSConfiguration.AuthValidationDate = $null
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


function New-TwitterErrorRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ResponseData]$ResponseData
    )

    function GetErrorData {
        param($ErrorList)
        $AllErrors = [System.Collections.Generic.List[hashtable]]::new()
            foreach ($AnError in $ErrorList) {
                $ThisError = @{}
                foreach ($Property in $AnError.psobject.Properties) {
                    $ThisError.Add($Property.Name,$Property.Value)
                }
                $AllErrors.Add($ThisError)
            }
        $AllErrors
    }

    $HttpStatusCode = $ResponseData.Status.value__.ToString()
    $ApiResponse  = $ResponseData.ApiResponse

    $ErrorId = 'APIv{0}-{1}' -f $ResponseData.ApiVersion,$ResponseData.Command
    $AllErrors = GetErrorData -ErrorList $ApiResponse.errors

    if ($ApiResponse.psobject.Properties.Name -notcontains 'data') {
        $IsTerminatingError = $true
    } else {
        $IsTerminatingError = $false
    }

    if ($ApiResponse.Type) {
        $ErrorMessage = $ApiResponse.Detail
        $ErrorCategory = Get-ErrorCategory -ErrorType $ApiResponse.Type
        $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory

        $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage
        $TwitterException.Source = $ResponseData.Command
        $TwitterException.Data.Add('TwitterApiError',$AllErrors)

        $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint)
        $ErrorRecord.ErrorDetails = $ErrorMessage

        $ErrorParams = @{
            ErrorRecord = $ErrorRecord
            CategoryActivity = $ResponseData.Command
        }

        if ($IsTerminatingError -and $TwitterErrors.Count -eq ($i + 1)) {
            $ErrorParams.Add('ErrorAction','Stop')
        }
        Write-Error @ErrorParams
    } else {
        $TwitterErrors = $ApiResponse.errors
        for ($i = 0; $i -le $TwitterErrors.Count; $i++) {

        #}
        #foreach ($TwitterError in $ApiResponse.errors) {
            switch ($ResponseData.ApiVersion) {
                1.1 {
                    $ErrorCategory = Get-ErrorCategory -StatusCode $HttpStatusCode -ErrorCode $TwitterErrors[$i].Code
                    if ($Twitter.Code -eq 415) {
                        $ErrorMessage = 'Message size exceeds limits of 10000 characters.'
                    } else {
                        $ErrorMessage = $TwitterErrors[$i].Message
                    }
                }
                2 {
                    $ErrorCategory = Get-ErrorCategory -ErrorType $TwitterErrors[$i].Type
                    $ErrorMessage = $TwitterErrors[$i].Detail
                }
            }

            $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory

            $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage
            $TwitterException.Source = $ResponseData.Command
            $TwitterException.Data.Add('TwitterApiError',$AllErrors)

            $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint)
            $ErrorRecord.ErrorDetails = $ErrorMessage

            $ErrorParams = @{
                ErrorRecord = $ErrorRecord
                CategoryActivity = $ResponseData.Command
            }

            if ($IsTerminatingError -and $TwitterErrors.Count -eq ($i + 1)) {
                $ErrorParams.Add('ErrorAction','Stop')
            }
            Write-Error @ErrorParams
        }
    }

}


function New-ValidationErrorRecord {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,
        [Parameter(Mandatory)]
        [string]$Target,
        [Parameter(Mandatory)]
        [string]$ErrorId
    )

    [System.Management.Automation.ErrorRecord]::new(
        [ValidationMetadataException]::new($Message),
        $ErrorId,
        'InvalidArgument',
        $Target
    )
}


function Set-BluebirdPSAuthUser {
    [CmdletBinding()]
    param()

    $Request = Invoke-TwitterVerifyCredentials
    if ($Request.Id) {
        $BluebirdPSConfiguration.AuthUserId = $Request.Id
        $BluebirdPSConfiguration.AuthUserName = switch ($BluebirdPSConfiguration.OutputType) {
            'CustomClasses' { $Request.UserName }
            'PSCustomObject' { $Request.screen_name }
            'JSON' { ($Request | ConvertFrom-Json -Depth 10).screen_name }
        }
        'Set AuthUserId ({0}), AuthUserName ({1})' -f $BluebirdPSConfiguration.AuthUserId,$BluebirdPSConfiguration.AuthUserName | Write-Verbose

        Export-BluebirdPSConfiguration

    } else {
        'Unable to set AuthUserId and AuthUserName' | Write-Warning
    }
}


function Set-TwitterMediaAltImageText {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Alias('media_id')]
        [string]$MediaId,

        [Parameter(Mandatory)]
        [ValidateLength(1,1000)]
        [string]$AltImageText
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://upload.twitter.com/1.1/media/metadata/create.json'
    }

    $Request.Body = '{{"media_id":"{0}","alt_text":{{"text":"{1}"}}}}' -f $MediaId,$AltImageText
    Invoke-TwitterRequest -RequestParameters $Request
}


function Write-TwitterResponse {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ResponseData]$ResponseData
    )

    try {

        if ($ResponseData.RateLimitRemaining -eq 0) {
            $RateLimitReached = 'Rate limit of {0} has been reached. Please wait until {1} before making another attempt for this resource.' -f $ResponseData.RateLimit,$ResponseData.RateLimitReset
            $RateLimitReached | Write-Error -ErrorAction Stop
        }

        if (($ResponseData.RateLimitRemaining -le $BluebirdPSConfiguration.RateLimitThreshold -and $null -ne $ResponseData.RateLimitRemaining)) {
            $RateLimitMessage = 'The rate limit for this resource is {0}. There are {1} remaining calls to this resource until {2}. ' -f $ResponseData.RateLimit, $ResponseData.RateLimitRemaining, $ResponseData.RateLimitReset
            switch ($BluebirdPSConfiguration.RateLimitAction) {
                0 { $RateLimitMessage | Write-Verbose -Verbose; break}
                1 { $RateLimitMessage | Write-Warning -Warning; break}
                2 { $RateLimitMessage | Write-Error ; break}
            }
        }

        $BluebirdPSHistoryList.Add($ResponseData)
        Write-Information -MessageData $ResponseData

        switch ($BluebirdPSConfiguration.OutputType) {
            'PSCustomObject' {
                $ResponseData.ApiResponse
                return
            }
            'JSON' {
                $ResponseData.ApiResponse | ConvertTo-Json -Depth 25
                return
            }
        }

        if ($LastStatusCode -eq 401) {
            New-TwitterErrorRecord -ResponseData $ResponseData
        } else {
            switch ($ResponseData.ApiVersion) {
                'oauth2' {
                    # Set-TwitterBearerToken - the only endpoint that uses oauth2
                    $ResponseData.ApiResponse
                    break
                }
                '1.1' {
                    if ($ResponseData.Command -eq 'Set-TwitterMutedUser') {
                        # return nothing as the returned v1.1 user 'muting' property may not have been updated
                        # an error will still be returned if an attempt to unmute a user that hasn't been muted
                        continue
                    } else {
                        [Helpers]::ParseApiV1Response($ResponseData.ApiResponse)
                    }
                    break
                }
                '2' {
                    if ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'following') {
                        [ResponseInfo]::GetUpdateFriendshipStatus($ResponseData)
                    } elseif ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'blocking') {
                        [ResponseInfo]::GetUserBlockStatus($ResponseData)
                    } elseif ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'liked') {
                        [ResponseInfo]::GetTweetLikeStatus($ResponseData)
                    } else {
                        [Helpers]::ParseApiV2Response($ResponseData.ApiResponse)
                    }
                    break
                }
            }
        }

    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    if ($ResponseData.ApiResponse.psobject.Properties.Name -contains 'errors') {
        New-TwitterErrorRecord -ResponseData $ResponseData
    }
}


function Invoke-TwitterRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.TwitterRequest]$RequestParameters
    )

    try {

        if ($RequestParameters.Body -and $RequestParameters.ContentType -eq 'application/json') {
            try {
                $RequestParameters.Body | ConvertFrom-Json -Depth 10 | Out-Null
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }

        switch ($RequestParameters.OAuthVersion) {
            'OAuth1a' {
                $Authentication =  [Authentication]::new(
                    $RequestParameters,
                    $OAuth['ApiKey'],$OAuth['ApiSecret'],
                    $OAuth['AccessToken'],$OAuth['AccessTokenSecret']
                )
            }
            'OAuth2Bearer' {
                $Authentication =  [Authentication]::new(
                    $RequestParameters,
                    $OAuth['BearerToken']
                )
            }
            'Basic' {
                $Authentication =  [Authentication]::new(
                    $RequestParameters,
                    $OAuth['ApiKey'],$OAuth['ApiSecret']
                )
            }

        }

        $WebRequestParams = @{
            Uri = $Authentication.Uri
            Method = $Authentication.HttpMethod
            Headers = @{ 'Authorization' = $Authentication.AuthHeader}
            ContentType = $RequestParameters.ContentType
            ResponseHeadersVariable = 'ResponseHeaders'
            StatusCodeVariable = 'StatusCode'
            SkipHttpErrorCheck = $true
            Verbose = $false
        }
        if ($RequestParameters.Form) {
            $WebRequestParams.Add('Form',$RequestParameters.Form)
        } elseif ($RequestParameters.Body) {
            $WebRequestParams.Add('Body',$RequestParameters.Body)
        }

        $ApiResponse = Invoke-RestMethod @WebRequestParams
        $script:LastStatusCode = $StatusCode
        $script:LastHeaders = $ResponseHeaders

        $ResponseData = [ResponseData]::new($RequestParameters,$Authentication,$ResponseHeaders,$LastStatusCode,$ApiResponse)
        Write-TwitterResponse -ResponseData $ResponseData

        if ($ResponseData.ApiResponse.psobject.Properties.Name -match 'meta|next_cursor') {

            $Progress = @{
                Activity = 'Retrieving paged results from Twitter API'
            }

            if ($RequestParameters.Endpoint -match '\/2\/' -and $null -ne $ResponseData.ApiResponse.meta.next_token) {

                # Twitter API V2 pagination
                if ($ResponseData.ApiResponse.meta.result_count) {
                    'Returned {0} objects' -f $ResponseData.ApiResponse.meta.result_count | Write-Verbose
                }

                # The endpoint /2/tweets/search/recent uses a different token for pagination
                # https://twittercommunity.com/t/why-does-timeline-use-pagination-token-while-search-uses-next-token/150963/2
                if ($RequestParameters.Endpoint -match 'tweets\/search\/recent') {
                    if ($RequestParameters.Query.Keys -match 'next_token') {
                        $RequestParameters.Query.Remove('next_token')
                    }
                    $NextPageKey = 'next_token'
                } else {
                    if ($RequestParameters.Query.Keys -match 'pagination_token') {
                        $RequestParameters.Query.Remove('pagination_token')
                    }
                    $NextPageKey = 'pagination_token'
                }
                $RequestParameters.Query.Add($NextPageKey,$ResponseData.ApiResponse.meta.next_token)

            } elseif ($null -ne $ResponseData.ApiResponse.next_cursor -and $ResponseData.ApiResponse.next_cursor -ne 0) {

                # Twitter API V1.1 cursoring, calls to endpoints will assume starting cursor of -1
                if ($RequestParameters.Query.Keys -match 'cursor') {
                    $RequestParameters.Query.Remove('cursor')
                }
                $RequestParameters.Query.Add('cursor',$ResponseData.ApiResponse.next_cursor)
            } else {
                return
            }

            Write-Progress @Progress
            Start-Sleep -Milliseconds (Get-Random -Minimum 300 -Maximum 600)
            Invoke-TwitterRequest -RequestParameters $RequestParameters
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Export-TwitterAuthentication {
    [CmdletBinding()]
    param()

    try {
        if (-Not (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath)) {
            $Action = 'new'
            New-Item -Path $BluebirdPSConfiguration.CredentialsPath -Force -ItemType File | Out-Null
        } else {
            $Action = 'existing'
        }

        [SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
        $OAuth | ConvertTo-Json | ConvertTo-SecureString -AsPlainText | ConvertFrom-SecureString | Set-Content -Path $BluebirdPSConfiguration.CredentialsPath -Force

        'Saved Twitter credentials to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.CredentialsPath | Write-Verbose

        $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime
        Export-BluebirdPSConfiguration

    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


function Import-TwitterAuthentication {
    [CmdletBinding()]
    param()

    'Checking for Twitter authentication.' | Write-Verbose

    $BluebirdPSAuthEnvPaths =
        'env:BLUEBIRDPS_API_KEY',
        'env:BLUEBIRDPS_API_SECRET',
        'env:BLUEBIRDPS_ACCESS_TOKEN',
        'env:BLUEBIRDPS_ACCESS_TOKEN_SECRET'

        $BluebirdPSBearerTokenEnvPath = 'env:BLUEBIRDPS_BEARER_TOKEN'

    if ((Test-Path -Path $BluebirdPSAuthEnvPaths) -notcontains $false) {
        'Importing Twitter authentication from environment variables.' | Write-Verbose

        $OAuth['ApiKey'] = $env:BLUEBIRDPS_API_KEY
        $OAuth['ApiSecret'] = $env:BLUEBIRDPS_API_SECRET
        $OAuth['AccessToken'] = $env:BLUEBIRDPS_ACCESS_TOKEN
        $OAuth['AccessTokenSecret'] = $env:BLUEBIRDPS_ACCESS_TOKEN_SECRET

        if (Test-Path -Path $BluebirdPSBearerTokenEnvPath) {
            $OAuth['BearerToken'] = $env:BLUEBIRDPS_BEARER_TOKEN
        }

    } elseif (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) {

        try {
            'Importing Twitter authentication from credentials file.' | Write-Verbose

            # read the encrypted credentials file, decrypt, and convert from JSON to object
            $OAuthFromDisk = Get-Content -Path $BluebirdPSConfiguration.CredentialsPath | ConvertTo-SecureString -ErrorAction Stop |
                ConvertFrom-SecureString -AsPlainText | ConvertFrom-Json

            # ensure that the credentials file has the correct keys/attributes
            foreach ($OAuthKey in 'ApiKey','ApiSecret','AccessToken','AccessTokenSecret','BearerToken') {
                if ($OAuthFromDisk.psobject.Properties.Name -notcontains $OAuthKey) {
                    Write-Error -ErrorAction Stop
                }
            }

            # ensure that we have values for the four required keys
            if ($OAuthFromDisk.psobject.Properties.Where{$_.Name -ne 'BearerToken' -and $null -ne $_.Value}.count -eq 4) {
                $OAuth['ApiKey'] = $OAuthFromDisk.ApiKey
                $OAuth['ApiSecret'] = $OAuthFromDisk.ApiSecret
                $OAuth['AccessToken'] = $OAuthFromDisk.AccessToken
                $OAuth['AccessTokenSecret'] = $OAuthFromDisk.AccessTokenSecret
            }

            if ($null -ne $OAuthFromDisk.BearerToken) {
                $OAuth['BearerToken'] = $OAuthFromDisk.BearerToken
            }
        }
        catch {
            'Unable to import Twitter authentication data from credentials file.',
            'Please use the Set-TwitterAuthentication command to update the required API keys and secrets.' | Write-Warning
            $PSCmdlet.ThrowTerminatingError($_)
        }
    } else {
        'Twitter authentication data was not discovered in environment variables or on disk in credentials file.',
        'Please use the Set-TwitterAuthentication command to set the required API keys and secrets.',
        'The authentication values will be encrypted and saved to disk.' | Write-Warning
        return
    }

    try {
        Invoke-TwitterVerifyCredentials | Out-Null
    }
    catch {
        'Twitter authentication data appears to be invalid.','Please use the Set-TwitterAuthentication command to update your stored credentials.' | Write-Warning
        $PSCmdlet.WriteError($_)
    }

    if ($null -eq $BluebirdPSConfiguration.AuthUserId) {
        Set-BluebirdPSAuthUser
    }

    if ($null -eq $OAuth['BearerToken']) {
        'Bearer token not present in Twitter authentication data.','Attempting to retrieve current bearer token from Twitter.' | Write-Verbose
        Set-TwitterBearerToken
    }

    try {
        Invoke-TwitterVerifyCredentials -BearerToken | Out-Null
    }
    catch {
        'Authentication data appears to have an invalid bearer token.','Please use the Set-TwitterBearerToken command to update your stored bearer token.' | Write-Warning
        $PSCmdlet.WriteError($_)
    }

    Export-BluebirdPSConfiguration
}


function Set-TwitterAuthentication {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [SecureString]$ApiKey = (Read-Host -Prompt 'API Key' -AsSecureString),
        [SecureString]$ApiSecret = (Read-Host -Prompt 'API Secret' -AsSecureString),
        [SecureString]$AccessToken = (Read-Host -Prompt 'Access Token' -AsSecureString),
        [SecureString]$AccessTokenSecret = (Read-Host -Prompt 'Access Token Secret' -AsSecureString)
    )

    try {
        $OAuth['ApiKey'] = $ApiKey | ConvertFrom-SecureString -AsPlainText
        $OAuth['ApiSecret'] = $ApiSecret | ConvertFrom-SecureString -AsPlainText
        $OAuth['AccessToken'] = $AccessToken | ConvertFrom-SecureString -AsPlainText
        $OAuth['AccessTokenSecret'] = $AccessTokenSecret | ConvertFrom-SecureString -AsPlainText

        if (Test-TwitterAuthentication) {
            'Successfully connected to Twitter.' | Write-Verbose

            Set-TwitterBearerToken
            Set-BluebirdPSAuthUser
            Export-TwitterAuthentication

        } else {
            'Failed authentication verification. Please check your credentials and try again.' | Write-Error -ErrorAction Stop
        }

    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Set-TwitterBearerToken {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param()

    try {

        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = 'https://api.twitter.com/oauth2/token'
            OAuthVersion = 'Basic'
            Body = 'grant_type=client_credentials'
            ContentType = 'application/x-www-form-urlencoded'
        }

        'Attempting to obtain an OAuth 2.0 bearer token.' | Write-Verbose

        $TwitterRequest = Invoke-TwitterRequest -RequestParameters $Request

        $OAuth['BearerToken'] = $TwitterRequest.access_token

        Export-TwitterAuthentication

        'OAuth 2.0 bearer token successfully set.' | Write-Verbose

    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Test-TwitterAuthentication {
    [CmdletBinding()]
    param(
        [switch]$BearerToken
    )

    Invoke-TwitterVerifyCredentials @PSBoundParameters | Out-Null
    if ($LastStatusCode -eq '200') {
        $true
        $BluebirdPSConfiguration.AuthValidationDate = Get-Date
    } else {
        $false
        $BluebirdPSConfiguration.AuthValidationDate = $null
    }

    Export-BluebirdPSConfiguration
}


function Get-TwitterDM {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string]$Id,
        [ValidateRange(1,50)]
        [int]$MessageCount = 20
    )

    if ($PSBoundParameters.ContainsKey('Id')) {
        $Request = [TwitterRequest]@{
            Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/show.json'
            Query = @{'id' = $Id }
        }
    } else {
        $Request = [TwitterRequest]@{
            Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/list.json'
            Query = @{'count'= $MessageCount }
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Publish-TwitterDM {
    [CmdletBinding(DefaultParameterSetName='DMUserId')]
    param(
        [string]$Message,

        [Parameter(Mandatory,ParameterSetName='DMUserId',ValueFromPipeline)]
        [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia',ValueFromPipeline)]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='DMUserObject',ValueFromPipeline)]
        [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [Parameter(ParameterSetName='DMUserId')]
        [Parameter(ParameterSetName='DMUserObject')]
        [ValidateNotNullOrEmpty()]
        [string]$MediaId,

        [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia')]
        [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia')]
        [ValidateScript({Test-Path -Path $_})]
        [string]$Path,

        [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia')]
        [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia')]
        [ValidateSet('DMImage','DMVideo','DMGif')]
        [string]$Category,

        [Parameter(ParameterSetName='DMUserIdWithMedia')]
        [Parameter(ParameterSetName='DMUserObjectWithMedia')]
        [ValidateLength(1,1000)]
        [string]$AltImageText
    )

    $MessageTemplate = '{{"event":{{"type":"message_create","message_create":{{"target":{{"recipient_id":{0}}},"message_data":{{"text":"{1}"}}}}}}}}'
    $MessageWithMediaTemplate = '{{"event":{{"type":"message_create","message_create":{{"target":{{"recipient_id":{0}}},"message_data":{{"text":"{1}","attachment":{{"type":"media","media":{{"id":{2}}}}}}}}}}}}}'

    if ($PSCmdlet.ParameterSetName -match 'WithMedia') {
        $TwitterMediaParams = @{
            Path = $Path
            Category = $Category
        }
        if ($AltImageText) {
            $TwitterMediaParams.Add('AltImageText',$AltImageText)
        }
        $MediaId = Send-TwitterMedia @TwitterMediaParams | Select-Object -ExpandProperty media_id
    }

    $RecipientId = $PSCmdlet.ParameterSetName -match 'DMUserObject' ? $User.Id : $Id
    $MessageText = [string]::IsNullOrEmpty($Message) ? [string]::Empty : $Message

    if ($MessageText) {
        if ($MediaId) {
            $Body = $MessageWithMediaTemplate -f $RecipientId,$MessageText,$MediaId
        } else {
            $Body = $MessageTemplate -f $RecipientId,$MessageText
        }
    } else {
        'You must provide a message, media, or a message and media. Please try again.' | Write-Warning
        return
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/new.json'
        Body = $Body.Replace("`r`n",'\n')
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Unpublish-TwitterDM {
    [CmdLetBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,
        [Parameter(Mandatory,ParameterSetName='ByDM',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.DirectMessage]$TwitterDM
    )

    $DMId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $TwitterDM.Id
    $Request = [TwitterRequest]@{
        HttpMethod = 'DELETE'
        Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/destroy.json'
        Query = @{ 'id' = $DMId}
    }

    if ($PSCmdlet.ShouldProcess($DMId, 'Removing direct message')) {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
        if ($LastStatusCode -eq 204) {
            'Successfully deleted message with id {0} for you only. You cannot delete a message from another user`s direct messages.' -f $DMId
        }
    }

}


function Add-TwitterList {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^([a-zA-Z0-9]|_|-){1,25}$', ErrorMessage = "The list name '{0}' is not valid. It must be 1-25 alphanumeric characters with underlines or dashes.")]
        [string]$Name,

        [ValidateNotNullOrEmpty()]
        [string]$Description,

        [ValidateSet('Private','Public')]
        [string]$Mode = 'Public'
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/lists/create.json'
        Query = @{
            'name' = $Name
            'mode' =  $Mode.ToLower()
            'description' = $Description
        }
    }

    'Creating list {0} with mode {1}' -f $Name,$Mode | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Add-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [string[]]$UserName
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Query = @{ 'screen_name' = $UserName -join ',' }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    if ($UserName.Count -gt 1) {
        $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/create_all.json'
        if ($UserName.Count -le 5) {
            $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName -join ','),$UserName.Count
        } else {
            $UserInfo = 'UserNames: {0}, Total Users: {1}' -f (($UserName[0..4] -join ',') + '...' ),$UserName.Count
        }
    } else {
        $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/create.json'
        $UserInfo = 'UserName: {0}' -f $UserName
    }

    'Adding users to list: {0} - {1}' -f $ListInfo,$UserInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Add-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/create.json'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add( 'list_id', $Id )
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add( 'list_id', $List.Id )
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    'Subscribing to list: {0}' -f $ListInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request | Out-Null
}


function Get-TwitterList {
    [CmdletBinding(DefaultParameterSetName='ByListUserName')]
    param(
        [Parameter(ParameterSetName='ByListUserName')]
        [string]$UserName,
        [Parameter(ParameterSetName='ByListUserName')]
        [switch]$OwnedListFirst,

        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='BySlug')]
        [ValidateNotNullOrEmpty()]
        [string]$Slug,
        [Parameter(ParameterSetName='BySlug')]
        [ValidateNotNullOrEmpty()]
        [string]$OwnerUserName,

        [Parameter(Mandatory,ParameterSetName='ByFullName')]
        [ValidateNotNullOrEmpty()]
        [string]$FullName
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/show.json'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ByListUserName' {
            $Request.Endpoint = 'https://api.twitter.com/1.1/lists/list.json'
            if ($UserName -ne [String]::Empty) {
                $Request.Query.Add('screen_name', $UserName)
            }
            if ($OwnedListFirst.IsPresent) {
                $Request.Query.Add( 'reverse', $true)
            }
        }
        'ById' {
            $Request.Query.Add( 'list_id', $Id )
        }
        'BySlug' {
            $Request.Query.Add( 'slug', $Slug)
            if ($PSBoundParameters.ContainsKey('OwnerUserName')) {
                $Request.Query.Add('owner_screen_name', $OwnerUserName)
            } else {
                $Request.Query.Add('owner_screen_name', $BluebirdPSConfiguration.AuthUserName)
            }
        }
        'ByFullName' {
            $UserName, $Slug = $FullName.Split('/')
            $Request.Query.Add( 'slug', $Slug)
            $Request.Query.Add( 'owner_screen_name', $UserName.Replace('^@',''))
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterListByOwner {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$UserName
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/ownerships.json'
        Query = @{
            screen_name = $PSBoundParameters.ContainsKey('UserName') ? $UserName : $BluebirdPSConfiguration.AuthUserName
            count = 1000
        }
    }

    'Getting lists owned by: {0}' -f $Request.Query.'screen_name' | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/members.json'
        Query = @{
            'skip_status' = $true
            'include_entities' = $true
            'count' = 5000
        }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add( 'list_id', $Id )
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add( 'list_id', $List.Id )
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    'Getting members of list: {0}' -f $ListInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request | Select-Object -ExpandProperty UserName
}


function Get-TwitterListMembership {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$UserName,

        [switch]$OwnedLists
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/memberships.json'
        Query = @{
            'cursor' = -1
            'count' = 1000
        }
    }
    if ($PSBoundParameters.ContainsKey('UserName')) {
        $Request.Query.Add( 'screen_name', $UserName )
        $UserInfo = $UserName
    } else {
        $UserInfo = $BluebirdPSConfiguration.AuthUserName
    }

    if ($OwnedLists.IsPresent) {
        $Request.Query.Add( 'filter_to_owned_lists', 'true' )
    }

    'Getting lists containing user: {0}' -f $UserInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterListSubscriber {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/subscribers.json'
        Query = @{
            'skip_status' = $true
            'include_entities' = $true
            'count' = 5000
        }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    'Getting subscribers for list: {0}' -f $ListInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request | Select-Object -ExpandProperty UserName
}


function Get-TwitterListSubscription {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$UserName
    )

    if ($PSBoundParameters.ContainsKey('UserName')) {
        $UserInfo = $UserName
    } else {
        $UserInfo = $BluebirdPSConfiguration.AuthUserName
    }

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/subscriptions.json'
        Query = @{
            count = 1000
            cursor = -1
            screen_name = $UserInfo
        }
    }

    'Getting list subscriptions for user: {0}' -f $UserInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterListTweets {
    [CmdletBinding(DefaultParameterSetName='ById')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param(

        [Parameter(Mandatory,ParameterSetName='ById')]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [string]$SinceId,
        [string]$MaxId,

        [switch]$ExcludeRetweets
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/statuses.json'
        Query = @{
            count = 200
        }
    }

    if ($PSBoundParameters.ContainsKey('SinceId')) {
        $Request.Query.Add('since_id',$SinceId)
    }
    if ($PSBoundParameters.ContainsKey('MaxId')) {
        $Request.Query.Add('max_id',$MaxId)
    }

    if ($ExcludeRetweets.IsPresent) {
        $Request.Query.Add('include_rts',$false)
        $RetweetInfo = ', including retweets,'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    'Getting tweets{0} for list: {1}' -f $RetweetInfo,$ListInfo | Write-Verbose
    Invoke-TwitterRequest -RequestParameters $Request
}


function Remove-TwitterList {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/lists/destroy.json'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $List = Get-TwitterList -Id $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
        }
    }

    if ($PSCmdlet.ShouldProcess($List.ToString(), 'Removing List')) {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
    }
}


function Remove-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [string[]]$UserName
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Query = @{ 'screen_name' = $UserName -join ',' }
    }

    if ($UserName.Count -gt 1) {
        $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/destroy_all.json'
        if ($UserName.Count -le 5) {
            $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName -join ','),$UserName.Count
        } else {
            $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName[0..4] -join ','),$UserName.Count
        }
    } else {
        $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/destroy.json'
        $UserInfo = 'UserName: {0}' -f $UserName
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    $Target = '{0} - {1}' -f $ListInfo,$UserInfo
    if ($PSCmdlet.ShouldProcess($Target, 'Remove users from list')) {
        Invoke-TwitterRequest -RequestParameters $Request
    }

}


function Remove-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/destroy.json'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    if ($PSCmdlet.ShouldProcess($ListInfo, 'Unsubscribing from list')) {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
    }

}


function Set-TwitterList {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [Parameter()]
        [ValidatePattern('^([a-zA-Z0-9]|_|-){1,25}$', ErrorMessage = "The list name '{0}' is not valid. It must be 1-25 alphanumeric characters with underlines or dashes.")]
        [string]$Name,

        [ValidateNotNullOrEmpty()]
        [string]$Description,

        [ValidateSet('Private','Public')]
        [string]$Mode
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/lists/update.json'
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
            $ListInfo = 'Id: {0}' -f $Id
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
            $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name
        }
    }

    $UpdatedProperties = 'Name','Description','Mode' | ForEach-Object {
        if ($PSBoundParameters.ContainsKey($_)) {
            if ($_ -eq 'Mode') {
                $Value = $PSBoundParameters[$_].ToString().ToLower()
            } else {
                $Value = $PSBoundParameters[$_]
            }
            $Request.Query.Add($_.ToLower(), $Value)
            $_
        }
    }

    if ($PSCmdlet.ShouldProcess(($UpdatedProperties -join ', '), ("Updating list {0} properties") -f $ListInfo)) {
        Invoke-TwitterRequest -RequestParameters $Request
    }
}


function Test-TwitterListMembership {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [ValidateNotNullOrEmpty()]
        [string]$UserName
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/members/show.json'
        Query = @{ 'include_entities' = 'false'; 'skip_status' = 'true' }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
        }
    }

    if ($PSBoundParameters.ContainsKey('UserName')) {
        $Request.Query.Add( 'screen_name', $UserName )
    } else {
        $Request.Query.Add( 'screen_name', $BluebirdPSConfiguration.AuthUserName )
    }

    try {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
        $true
    }
    catch {
        $false
    }
}


function Test-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.List]$List,

        [ValidateNotNullOrEmpty()]
        [string]$UserName
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/show.json'
        Query = @{ 'include_entities' = 'false'; 'skip_status' = 'true' }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Request.Query.Add('list_id',$Id)
        }
        'ByList' {
            $Request.Query.Add('list_id',$List.Id)
        }
    }

    if ($PSBoundParameters.ContainsKey('UserName')) {
        $Request.Query.Add( 'screen_name', $UserName )
    } else {
        $Request.Query.Add( 'screen_name', $BluebirdPSConfiguration.AuthUserName )
    }

    try {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
        $true
    }
    catch {
        $false
    }
}


function Send-TwitterMedia {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateScript({Resolve-Path -Path $_})]
        [string]$Path,

        [Parameter(Mandatory)]
        [ValidateSet('TweetImage','TweetVideo','TweetGif','DMImage','DMVideo','DMGif')]
        [string]$Category,

        [ValidateLength(1,1000)]
        [string]$AltImageText
    )

    begin {

        $MediaFileInfo = Get-ChildItem $Path

        # get mime type by extension, see https://github.com/SCRT-HQ/PSGSuite/blob/master/PSGSuite/Private/Get-MimeType.ps1 for inspiration
        # there's nothing currently in .Net Core that could derive the type from the content
        $MediaMimeTypes = @{
            gif = 'image/gif'
            jpg = 'image/jpeg'
            jpeg = 'image/jpeg'
            png = 'image/png'
            webp = 'image/webp'
            mp4 = 'video/mp4'
            mov = 'video/quicktime'
        }
        $MimeType = $MediaMimeTypes[$MediaFileInfo.Extension.TrimStart('.')]

        # validate size of file
        # validate if detected mimetype matches category
        $SizeLimitExceededMessage = 'The size of media {0} exceeded the limit of {2} bytes. Please try again.'
        $CategoryMimeTypeMismatch = 'Category {0} does not match the media mimetype of {1}. Please try again.'
        $CategoryAltImgText = 'Category {0} does not allow the AltImageText. Please try again.'
        $ValidationErrorRecord = @{
            Message = [String]::Empty
            Target = $MediaFileInfo.Name
            ErrorId = $null
        }

        switch -regex ($Category) {
            'Image' {
                if ($MediaFileInfo.Length -gt 5MB) {
                    $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,5MB
                    $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                if ($MimeType -notmatch 'image') {
                    $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType
                    $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                break
            }
            'Video' {
                if ($MediaFileInfo.Length -gt 512MB) {
                    $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,512MB
                    $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                if ($MimeType -notmatch 'video') {
                    $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType
                    $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                break
            }
            'Gif' {
                if ($MediaFileInfo.Length -gt 15MB) {
                    $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,15MB
                    $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                if ($MimeType -ne 'image/gif') {
                    $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType
                    $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch'
                    $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
                }
                break
            }
        }

        if ($PSBoundParameters.ContainsKey('AltImageText') -and $MimeType -match 'video') {
            $ValidationErrorRecord.Message = $CategoryAltImgText -f $Category,$MimeType
            $ValidationErrorRecord.ErrorId = 'MediaCategoryNoSupportForAltImgText'
            $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord))
        }

        $MediaCategory = switch ($Category) {
            'TweetImage' { 'tweet_image' }
            'TweetVideo' { 'tweet_video' }
            'TweetGif'   { 'tweet_gif' }
            'DMImage'    { 'dm_image' }
            'DMVideo'    { 'dm_video' }
            'DMGif'      { 'dm_gif' }
        }
        $MediaUploadUrl = 'https://upload.twitter.com/1.1/media/upload.json'
        $TotalBytes = $MediaFileInfo.Length
    }

    process {

        'Reading file {0}' -f $MediaFileInfo.FullName | Write-Verbose
        # read the image into memory
        $BufferSize = 900000
        $Buffer = [Byte[]]::new($BufferSize)
        $Reader = [System.IO.File]::OpenRead($MediaFileInfo.FullName)
        $Media = [ArrayList]::new()
        do {
            $BytesRead = $Reader.Read($Buffer, 0 , $BufferSize)
            $null = $Media.Add([Convert]::ToBase64String($Buffer, 0, $BytesRead))
        } while ($BytesRead -eq $BufferSize)
        $Reader.Dispose()

        # ------------------------------------------------------------------------------------------
        # INIT phase
        'Beginning INIT phase - media size {0}, category {1}, type {2}' -f $TotalBytes,$MediaCategory,$MimeType | Write-Verbose
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = $MediaUploadUrl
            Form = @{
                command = 'INIT'
                total_bytes = $TotalBytes
                media_category = $MediaCategory
                media_type = $MimeType
            }
        }

        try {
            $SendMediaInitResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false
            if ($SendMediaInitResult-is [ErrorRecord]) {
                $PSCmdlet.ThrowTerminatingError($SendMediaInitResult)
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }

        $MediaId = $SendMediaInitResult.'media_id'
        'Upload for media id {0} successfully initiated' -f $MediaId | Write-Verbose

        # ------------------------------------------------------------------------------------------
        # APPEND phase
        'Beginning APPEND phase' | Write-Verbose
        $Index = 0
        foreach ($Chunk in $Media) {

            $PercentComplete = (($Index + 1) / $Media.Count) * 100
            $Activity = "Uploading media file '{0}' with id {1}" -f $MediaFileInfo.Name,$MediaId
            $CurrentOperation = "Media chunk #{0}" -f $Index
            $Status = "{0}% Complete:" -f $PercentComplete
            Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status -PercentComplete $PercentComplete

            $Request = [TwitterRequest]@{
                HttpMethod = 'POST'
                Endpoint = $MediaUploadUrl
                Form = @{
                    command = 'APPEND'
                    media_id = $MediaId
                    media_data = $Media[$Index]
                    segment_index = $Index
                }
            }

            $SendMediaAppendResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false

            if ($SendMediaAppendResult -is [ErrorRecord]) {
                $PSCmdlet.ThrowTerminatingError($SendMediaAppendResult)
            }
            $Index++
        }
        Write-Progress -Activity 'Media upload append phase completed' -Completed

        # ------------------------------------------------------------------------------------------
        # FINALIZE phase
        'Beginning FINALIZE phase' | Write-Verbose
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = $MediaUploadUrl
            Form = @{
                command = 'FINALIZE'
                media_id = $MediaId
            }
        }

        $SendMediaFinalizeResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false
        if ($SendMediaFinalizeResult -is [ErrorRecord]) {
            $PSCmdlet.ThrowTerminatingError($SendMediaFinalizeResult)
        }

        # ------------------------------------------------------------------------------------------
        # STATUS phase
        if ($SendMediaFinalizeResult.'processing_info'.'check_after_secs') {
            'Beginning STATUS phase' | Write-Verbose
            $WaitSeconds = $SendMediaFinalizeResult.'processing_info'.'check_after_secs' -as [int]
            $SendMediaStatus = Get-SendMediaStatus -MediaId $MediaId -WaitSeconds $WaitSeconds -Verbose:$false
            $SendMediaCompletionResults = $SendMediaStatus
        } else {
            $SendMediaCompletionResults = $SendMediaFinalizeResult
        }

        # ------------------------------------------------------------------------------------------
        # Add AltImageText phase
        if ($AltImageText.Length -gt 0) {
            'Adding AltImageText to media {0}' -f $MediaId | Write-Verbose
            Set-TwitterMediaAltImageText -MediaId $MediaId -AltImageText $AltImageText -Verbose:$false | Out-Null
            if ($LastStatusCode -eq '200') {
                'Alt image text successfully added to media' | Write-Verbose
            }
        }

        'Media upload complete' | Write-Verbose
        $SendMediaCompletionResults
    }

    end {

    }
}


function Add-TwitterSavedSearch {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/saved_searches/create.json'
        Query = @{ query = $SearchString }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function  Get-TwitterSavedSearch {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string]$Id
    )

    if ($PSBoundParameters.ContainsKey('Id')) {
        $Endpoint = 'https://api.twitter.com/1.1/saved_searches/show/{0}.json' -f $Id
    } else {
        $Endpoint = 'https://api.twitter.com/1.1/saved_searches/list.json'
    }

    $Request = [TwitterRequest]@{
        Endpoint = $Endpoint
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Remove-TwitterSavedSearch {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Id,
        [Parameter(Mandatory,ParameterSetName='BySavedSearch',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV1.SavedSearch]$SavedSearch
    )

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $SavedSearch = Get-TwitterSavedSearch -Id $Id
    }

    $SearchInfo = 'Search: {0}, Created: {1}' -f $SavedSearch.Query,$SavedSearch.CreatedAt
    if ($SavedSearch) {
        if ($PSCmdlet.ShouldProcess($SearchInfo, 'Removing Saved Search')) {
            $Request = [TwitterRequest]@{
                HttpMethod = 'POST'
                Endpoint = 'https://api.twitter.com/1.1/saved_searches/destroy/{0}.json' -f $SavedSearch.Id
            }
            Invoke-TwitterRequest -RequestParameters $Request | Out-Null
        }
    } else {
        'No saved search found with SearchId of {0}' -f $ThisSearchId | Write-Warning
    }

}


function Get-TwitterAccountSettings {
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param()

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/account/settings.json'
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterPermissions {
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param()
    try {
        $AccessLevel = $LastHeaders.'x-access-level'
        switch ($AccessLevel) {
            'read-write-directmessages' { 'Read/Write/DirectMessages'}
            'read-write' { 'Read/Write' }
            'read' { 'ReadOnly' }
        }
    } catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}



function Get-TwitterRateLimitStatus {
    [CmdletBinding()]
    param(
        [ValidateSet(
            'lists','application','mutes','live_video_stream','friendships','guide','auth','blocks','geo',
            'users','teams','followers','collections','statuses','custom_profiles','webhooks','contacts',
            'labs','i','tweet_prompts','moments','limiter_scalding_report_creation','fleets','help','feedback',
            'business_experience','graphql&POST','friends','sandbox','drafts','direct_messages','media','traffic',
            'account_activity','account','safety','favorites','device','tweets','saved_searches','oauth','search','trends','live_pipeline','graphql'
        )]
        [string[]]$Resources
    )

    if ($Resources.Count -gt 0) {
        $Request = [TwitterRequest]@{
            Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json'
            Query = @{ 'resources' = ($Resources -join ',') }
        }
    } else {
        $Request = [TwitterRequest]@{
            Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json'
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterUserProfileBanner {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$UserName
    )

    if (-Not $PSBoundParameters.ContainsKey('UserName')) {
        $Query = @{ 'screen_name' = $BluebirdPSConfiguration.AuthUserName }
    } else {
        $Query = @{ 'screen_name' = $UserName }
    }

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/users/profile_banner.json'
        Query = $Query
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Publish-Tweet {
    [CmdletBinding(DefaultParameterSetName='Tweet')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateLength(1,10000)]
        [string]$TweetText,

        [Parameter()]
        [string]$ReplyToTweet,

        [Parameter(ParameterSetName='Tweet')]
        [string[]]$MediaId,

        [Parameter(Mandatory,ParameterSetName='TweetWithMedia')]
        [ValidateScript({Test-Path -Path $_})]
        [string]$Path,

        [Parameter(Mandatory,ParameterSetName='TweetWithMedia')]
        [ValidateSet('TweetImage','TweetVideo','TweetGif')]
        [string]$Category,

        [Parameter(ParameterSetName='TweetWithMedia')]
        [ValidateLength(1,1000)]
        [string]$AltImageText

    )

    # https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
    # maximum of 4 pics, or 1 gif, or 1 video

    # count $TweetText characters
    # if the count is greater than allowed, suggest Send-TweetThread and fail

    if ($PSCmdlet.ParameterSetName -eq 'TweetWithMedia') {
        $SendMediaParams = @{
            Path = $Path
            Category = $Category
        }
        if ($PSBoundParameters.ContainsKey('AltImageText')) {
            $SendMediaParams.Add('AltImageText',$AltImageText)
        }
        $MediaId = Send-TwitterMedia @SendMediaParams | Select-Object -ExpandProperty media_id
    }

    $Query = @{
        status = $TweetText
    }

    if ($PSBoundParameters.ContainsKey('ReplyToTweet')) {
        $Query.Add('in_reply_to_status_id', $ReplyToTweet)

        # this will use the tweet id to get the screen_name and append it to the @mentions until @mentions have reached the limit.
        $Query.Add('auto_populate_reply_metadata', 'true')
    }

    if ($MediaId.Count -gt 0) {
        $Query.Add('media_ids', ($MediaId -join ','))
    }
    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/statuses/update.json'
        Query = $Query
    }

    try {
        $Tweet = Invoke-TwitterRequest -RequestParameters $Request
        Get-Tweet -Id $Tweet.id
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Set-Retweet {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName='Retweet')]
    param(
        [Parameter(Mandatory)]
        [string]$Id,
        [Parameter(ParameterSetName='Retweet')]
        [switch]$Retweet,
        [Parameter(ParameterSetName='Unretweet')]
        [switch]$Unretweet
    )

    if ($PSCmdlet.ParameterSetName -eq 'Retweet') {
        $Endpoint = 'https://api.twitter.com/1.1/statuses/retweet/{0}.json' -f $Id
    } else {
        $Endpoint = 'https://api.twitter.com/1.1/statuses/unretweet/{0}.json' -f $Id
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = $Endpoint
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Unpublish-Tweet {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [string]$Id,
        [Parameter(Mandatory,ParameterSetName='ByTweet',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet
    )

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $TweetId = $Id
        $TweetInfo = 'Id: {0}' -f $Id
    } else {
        $TweetId = $Tweet.Id
        $TweetInfo = 'Id: {0}, CreatedAt: {1}' -f $Tweet.Id,$Tweet.CreatedAt
    }

    if ($PSCmdlet.ShouldProcess($TweetInfo, 'Deleting Tweet')) {
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = 'https://api.twitter.com/1.1/statuses/destroy/{0}.json' -f $TweetId
        }
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
    }
}



function Get-TwitterFriendship {
    [CmdletBinding(DefaultParameterSetName='Lookup')]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='Lookup')]
        [ValidateCount(1,100)]
        [string[]]$UserName,

        [Parameter(Mandatory,ParameterSetName='Show')]
        [string]$SourceUserName,

        [Parameter(Mandatory,ParameterSetName='Show')]
        [string]$TargetUserName,

        [Parameter(ParameterSetName='Incoming')]
        [switch]$Incoming,

        [Parameter(ParameterSetName='Pending')]
        [switch]$Pending,

        [Parameter(ParameterSetName='NoRetweets')]
        [switch]$NoRetweets

    )

    $Query = @{}

    switch -Regex ($PSCmdlet.ParameterSetName) {
        'Lookup' {
            $Endpoint = 'https://api.twitter.com/1.1/friendships/lookup.json'
            $Query.Add('screen_name',($UserName -join ','))
        }
        'Show' {
            $Endpoint = 'https://api.twitter.com/1.1/friendships/show.json'
            $Query.Add('source_screen_name',$SourceUserName)
            $Query.Add('target_screen_name',$TargetUserName)
        }
        'Incoming' {
            $Endpoint = 'https://api.twitter.com/1.1/friendships/incoming.json'
        }
        'Pending' {
            $Endpoint = 'https://api.twitter.com/1.1/friendships/outgoing.json'
        }
        'NoRetweets' {
            $Endpoint = 'https://api.twitter.com/1.1/friendships/no_retweets/ids.json'
        }
    }

    $Request = [TwitterRequest]@{
        Endpoint = $Endpoint
        Query = $Query
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterMutedUser {
    [CmdletBinding()]
    param()

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/1.1/mutes/users/ids.json'
        Query = @{ cursor = -1 }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Set-TwitterMutedUser {
    [CmdletBinding(DefaultParameterSetName='Mute')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [Parameter(ParameterSetName='Mute')]
        [switch]$Mute,

        [Parameter(Mandatory,ParameterSetName='Unmute')]
        [switch]$Unmute
    )

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Query = @{ 'screen_name' = $User.UserName }
    }

    if ($PSCmdlet.ParameterSetName -eq 'Mute') {
        $Request.Endpoint = 'https://api.twitter.com/1.1/mutes/users/create.json'
    } else {
        $Request.Endpoint = 'https://api.twitter.com/1.1/mutes/users/destroy.json'
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Submit-TwitterUserAsSpam {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [BluebirdPS.APIV2.UserInfo.User]$User,
        [switch]$Block
    )

    $Action = 'Report as Spam'
    if($Block.IsPresent) {
        $Action += ' and Block'
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/1.1/users/report_spam.json'
        Query = @{
            screen_name = $User.UserName
            perform_block = $Block
        }
    }
    $Target = '{0}, CreatedAt: {1}, Description: {2}' -f $User.UserName,$User.CreatedAt,$User.Description

    if ($PSCmdlet.ShouldProcess($Target, $Action)) {
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
    }
}


function Get-Tweet {
    [CmdletBinding(DefaultParameterSetName='Tweet')]
    param(
        [Parameter(Mandatory,Position=0,ParameterSetName='Tweet')]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")]
        [string[]]$Id,
        [Parameter(Mandatory,Position=0,ParameterSetName='Conversation')]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The conversation tweet Id '{0}' is not valid.")]
        [string]$ConversationId,
        [switch]$NonPublicMetrics,
        [switch]$PromotedMetrics,
        [switch]$OrganicMetrics,
        [switch]$IncludeExpansions
    )

    $Request = [TwitterRequest]@{
        ExpansionType = 'Tweet'
        NonPublicMetrics = $NonPublicMetrics
        PromotedMetrics = $PromotedMetrics
        OrganicMetrics = $OrganicMetrics
        IncludeExpansions = $IncludeExpansions
    }

    switch ($PSCmdlet.ParameterSetName) {
        'Tweet' {
            if ($Id.Count -gt 1) {
                $Request.Query.Add('ids', ($Id -join ','))
                $Request.Endpoint = 'https://api.twitter.com/2/tweets'
            } else {
                $Request.Endpoint = 'https://api.twitter.com/2/tweets/{0}' -f $Id
            }
        }
        'Conversation' {
            $OriginalTweet = Get-Tweet -Id $ConversationId
            $OriginalTweet
            if ($OriginalTweet.CreatedAt -lt (Get-Date).AddDays(-7)) {
                'As searching by ConversationId is based on recent search from the Standard product track, you can only retreive a conversation that started within the last 7 days.' | Write-Warning
                return
            }
            $Request.Query.Add('query',('conversation_id:{0}' -f $ConversationId))
            $Request.Endpoint = 'https://api.twitter.com/2/tweets/search/recent'
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TweetCount {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString,

        [ValidateNotNullOrEmpty()]
        [ValidateSet('Minute','Hour','Day')]
        [string]$Granularity = 'Hour',

        [ValidateNotNullOrEmpty()]
        [datetime]$StartTime,
        [ValidateNotNullOrEmpty()]
        [datetime]$EndTime,

        [ValidateNotNullOrEmpty()]
        [string]$SinceId,
        [ValidateNotNullOrEmpty()]
        [string]$UntilId,

        [Parameter(Mandatory,ParameterSetName='Summary')]
        [switch]$Summary,

        [Parameter(Mandatory,ParameterSetName='CountOnly')]
        [switch]$CountOnly
    )

    $Request = [TwitterRequest]@{
        OAuthVersion = 'OAuth2Bearer'
        Endpoint = 'https://api.twitter.com/2/tweets/counts/recent'
        Query = @{
            query = $SearchString
            granularity = $Granularity.ToLower()
        }
    }

    if ($PSBoundParameters.ContainsKey('StartTime')) {
        $Request.Query.Add('start_time',[Helpers]::ConvertToV1Date($StartTime))
    }
    if ($PSBoundParameters.ContainsKey('EndTime')) {
        $Request.Query.Add('end_time',[Helpers]::ConvertToV1Date($EndTime))
    }
    if ($PSBoundParameters.ContainsKey('SinceId')) {
        $Request.Query.Add('since_id',$SinceId)
    }
    if ($PSBoundParameters.ContainsKey('UntilId')) {
        $Request.Query.Add('until_id',$UntilId)
    }

    $TweetCount = Invoke-TwitterRequest -RequestParameters $Request
    $TotalCount = (Get-BluebirdPSHistory -Last 1).ApiResponse.meta.total_tweet_count
    $TweetCountSummary = [TweetInfo.TweetCountSummary]@{
        SearchString = $SearchString
        Granularity = $Granularity
        StartTime = ($TweetCount.Start | Select-Object -First 1)
        EndTime = ($TweetCount.End | Select-Object -Last 1)
        TotalCount = $TotalCount
    }

    switch ($PSCmdlet.ParameterSetName) {
        'Summary' {
            $TweetCountSummary
        }
        'CountOnly' {
            $TotalCount
        }
        default {
            $TweetCount
        }
    }
}


function Get-TweetLikes {
    [CmdLetBinding(DefaultParameterSetName='ById')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")]
        [string]$Id,

        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet,

        [switch]$IncludeExpansions
    )

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $TweetId = $Id
        }
        'ByTweet' {
            $TweetId = $Tweet.Id
        }
    }

    $Request = [TwitterRequest]@{
        ExpansionType = 'User'
        Endpoint = 'https://api.twitter.com/2/tweets/{0}/liking_users' -f $TweetId
        IncludeExpansions = $IncludeExpansions
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TweetPoll {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")]
        [string]$Id,

        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet
    )

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $TweetId = $Id
        }
        'ByTweet'   {
            $TweetId = $Tweet.Id
        }
    }

    Get-Tweet -Id $TweetId -IncludeExpansions | Where-Object { $_.psobject.TypeNames -contains 'BluebirdPS.APIV2.Objects.Poll' }
}


function Get-TwitterTimeline {
    [CmdletBinding(DefaultParameterSetName='User')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [Parameter(ParameterSetName='User')]
        [ValidateSet('Retweets','Replies')]
        [string[]]$Exclude,

        [Parameter(Mandatory,ParameterSetName='Mentions')]
        [switch]$Mentions,

        [ValidateNotNullOrEmpty()]
        [datetime]$StartTime,
        [ValidateNotNullOrEmpty()]
        [datetime]$EndTime,

        [ValidateNotNullOrEmpty()]
        [string]$SinceId,
        [ValidateNotNullOrEmpty()]
        [string]$UntilId,

        [switch]$IncludeExpansions,
        [switch]$NonPublicMetrics,
        [switch]$PromotedMetrics,
        [switch]$OrganicMetrics
    )

    $Request = [TwitterRequest]@{
        Endpoint = $Endpoint
        ExpansionType = 'Tweet'
        NonPublicMetrics = $NonPublicMetrics
        PromotedMetrics = $PromotedMetrics
        OrganicMetrics = $OrganicMetrics
        IncludeExpansions = $IncludeExpansions
        Query = @{ 'max_results' = 100 }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'User' {
            $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/tweets' -f $User.Id
            if ($PSBoundParameters.ContainsKey('Exclude')){
                $Request.Query.Add('exclude', ($Exclude.ToLower() -join ',') )
            }
        }
        'Mentions' {
            $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/mentions' -f $User.Id
        }
    }

    if ($PSBoundParameters.ContainsKey('StartTime')) {
        $Request.Query.Add('start_time',[Helpers]::ConvertToV1Date($StartTime))
    }
    if ($PSBoundParameters.ContainsKey('EndTime')) {
        $Request.Query.Add('end_time',[Helpers]::ConvertToV1Date($EndTime))
    }
    if ($PSBoundParameters.ContainsKey('SinceId')) {
        $Request.Query.Add('since_id',$SinceId)
    }
    if ($PSBoundParameters.ContainsKey('UntilId')) {
        $Request.Query.Add('until_id',$UntilId)
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Search-Tweet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString,

        [ValidateRange(10,100)]
        [int]$MaxResults=100,

        [switch]$NonPublicMetrics,
        [switch]$PromotedMetrics,
        [switch]$OrganicMetrics,
        [switch]$IncludeExpansions
    )

    $Request = [TwitterRequest]@{
        ExpansionType = 'Tweet'
        Endpoint = 'https://api.twitter.com/2/tweets/search/recent'
        Query =  @{
            'query' = $SearchString
            'max_results' = $MaxResults
        }
        NonPublicMetrics = $NonPublicMetrics
        PromotedMetrics = $PromotedMetrics
        OrganicMetrics = $OrganicMetrics
        IncludeExpansions = $IncludeExpansions
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Set-TweetLike {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName='LikeById')]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeById')]
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeById')]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")]
        [string]$Id,

        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeByTweet')]
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeByTweet')]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet,

        [Parameter(ParameterSetName='LikeById')]
        [Parameter(ParameterSetName='LikeByTweet')]
        [switch]$Like,

        [Parameter(Mandatory,ParameterSetName='UnlikeById')]
        [Parameter(Mandatory,ParameterSetName='UnlikeByTweet')]
        [switch]$Unlike
    )

    if ($PSCmdlet.ParameterSetName -match 'Id$') {
        $TweetId = $Id
    } else {
        $TweetId = $Tweet.Id
    }
    if ($PSCmdlet.ParameterSetName -match '^Like') {
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = 'https://api.twitter.com/2/users/{0}/likes' -f $BluebirdPSConfiguration.AuthUserId
            Body =  '{{"tweet_id": "{0}"}}' -f $TweetId
        }
    } else {
        $Request = [TwitterRequest]@{
            HttpMethod = 'DELETE'
            Endpoint = 'https://api.twitter.com/2/users/{0}/likes/{1}' -f $BluebirdPSConfiguration.AuthUserId,$TweetId
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Set-TweetReply {
    [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName='Hide')]
    param(
        [Parameter(Mandatory)]
        [string]$Id,
        [Parameter(ParameterSetName='Hide')]
        [switch]$Hide,
        [Parameter(ParameterSetName='Show')]
        [switch]$Show
    )

    switch ($PSCmdlet.ParameterSetName) {
        'Hide' { $Body = '{"hidden": true}'  }
        'Show' { $Body = '{"hidden": false}' }
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'PUT'
        Endpoint = 'https://api.twitter.com/2/tweets/{0}/hidden' -f $Id
        Body = $Body
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Add-TwitterFriend {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User
    )

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $Body = '{{"target_user_id": "{0}"}}' -f $Id
        }
        'ByUser' {
            $Body = '{{"target_user_id": "{0}"}}' -f $User.Id
        }
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId
        Body = $Body
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterBlockedUser {
    [CmdletBinding()]
    param(
        [switch]$IncludeExpansions
    )

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId
        ExpansionType = 'User'
        IncludeExpansions = $IncludeExpansions
        Query = @{ 'max_results' = 1000 }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterFollowers {
    [CmdletBinding(DefaultParameterSetName='ById')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param(
        [Parameter(ParameterSetName='ById',ValueFromPipeline)]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [switch]$IncludeExpansions
    )

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            if ($PSBoundParameters.ContainsKey('Id')) {
                $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $Id
            } else {
                $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $BluebirdPSConfiguration.AuthUserId
            }
        }
        'ByUser' {
            $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $User.Id
        }
    }

    $Request = [TwitterRequest]@{
        ExpansionType = 'User'
        Endpoint = $Endpoint
        Query = @{'max_results' = 1000 }
        IncludeExpansions = $IncludeExpansions
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterFriends {
    [CmdletBinding(DefaultParameterSetName='ById')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param(
        [Parameter(ParameterSetName='ById',ValueFromPipeline)]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [switch]$IncludeExpansions
    )

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            if ($PSBoundParameters.ContainsKey('Id')) {
                $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $Id
            } else {
                $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId
            }
        }
        'ByUser' {
            $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $User.Id
        }
    }

    $Request = [TwitterRequest]@{
        ExpansionType = 'User'
        Endpoint = $Endpoint
        Query = @{'max_results' = 1000 }
        IncludeExpansions = $IncludeExpansions
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterUser {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [ValidateCount(1, 100)]
        [string[]]$User,
        [switch]$IncludeExpansions
    )

    begin {
        $UserNames = [List[string]]::new()
        $UserIds = [List[string]]::new()
    }

    process {
        foreach ($ThisUser in $User) {
            try {
                [long]::Parse($ThisUser) | Out-Null
                $UserIds.Add($ThisUser)
            }
            catch {
                $UserNames.Add($ThisUser)
            }
        }
    }

    end {
        if  ($UserNames.Count -eq 0 -and $UserIds.Count -eq 0) {
            $Request = [TwitterRequest]@{
                ExpansionType     = 'User'
                IncludeExpansions = $IncludeExpansions
                Endpoint = 'https://api.twitter.com/2/users/by/username/{0}' -f $BluebirdPSConfiguration.AuthUserName
            }
            $Request.SetCommandName('Get-TwitterUser')
            Invoke-TwitterRequest -RequestParameters $Request
        }

        if ($UserNames.Count -gt 0) {
            $Request = [TwitterRequest]@{
                ExpansionType     = 'User'
                IncludeExpansions = $IncludeExpansions
            }
            if ($UserNames.Count -eq 1) {
                $Request.Endpoint = 'https://api.twitter.com/2/users/by/username/{0}' -f $UserNames[0]
            } else {
                $Request.Endpoint = 'https://api.twitter.com/2/users/by'
                $Request.Query = @{'usernames' = $UserNames -join ',' }
            }
            $Request.SetCommandName('Get-TwitterUser')
            Invoke-TwitterRequest -RequestParameters $Request
        }

        if ($UserIds.Count -gt 0) {
            $Request = [TwitterRequest]@{
                ExpansionType     = 'User'
                IncludeExpansions = $IncludeExpansions
            }
            if ($UserIds.Count -eq 1) {
                $Request.Endpoint = 'https://api.twitter.com/2/users/{0}' -f $UserIds[0]
            } else {
                $Request.Endpoint = 'https://api.twitter.com/2/users'
                $Request.Query = @{'ids' = $UserIds -join ',' }
            }
            $Request.SetCommandName('Get-TwitterUser')
            Invoke-TwitterRequest -RequestParameters $Request
        }
    }
}


function Remove-TwitterFriend {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [string]$Id,

        [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User
    )

    $UserId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $User.Id

    $Request = [TwitterRequest]@{
        HttpMethod = 'DELETE'
        Endpoint = 'https://api.twitter.com/2/users/{0}/following/{1}' -f $BluebirdPSConfiguration.AuthUserId,$UserId
    }

    if ($PSCmdlet.ShouldProcess($UserId, 'Unfollow user')) {
        Invoke-TwitterRequest -RequestParameters $Request
    }
}


function Set-TwitterBlockedUser {
    [CmdletBinding(DefaultParameterSetName='Block')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [Parameter(ParameterSetName='Block')]
        [switch]$Block,

        [Parameter(Mandatory,ParameterSetName='Unblock')]
        [switch]$Unblock
    )

   if ($PSCmdlet.ParameterSetName -eq 'Block') {
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId
            Body = '{{"target_user_id": "{0}"}}' -f $User.Id
        }
    } else {
        $Request = [TwitterRequest]@{
            HttpMethod = 'DELETE'
            Endpoint = 'https://api.twitter.com/2/users/{0}/blocking/{1}' -f $BluebirdPSConfiguration.AuthUserId,$User.Id
        }
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function ConvertFrom-EpochTime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$UnixTime
    )

    if ($UnixTime.Length -eq 10) {
        [DateTimeOffset]::FromUnixTimeSeconds([long]::Parse($UnixTime)).ToLocalTime().DateTime
    } else {
        [DateTimeOffset]::FromUnixTimeMilliseconds([long]::Parse($UnixTime)).ToLocalTime().DateTime
    }
}


function ConvertFrom-TwitterV1Date {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Date
    )

    try {
        [datetime]::ParseExact( $Date, "ddd MMM dd HH:mm:ss zzz yyyy", [CultureInfo]::InvariantCulture )
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Export-BluebirdPSConfiguration {
    [CmdletBinding()]
    param()

    try {
        if (-Not (Test-Path -Path $BluebirdPSConfiguration.ConfigurationPath)) {
            $Action = 'new'
            New-Item -Path $BluebirdPSConfiguration.ConfigurationPath -Force -ItemType File | Out-Null
        } else {
            $Action = 'existing'
        }

        if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) {
            $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime
        }

        $BluebirdPSConfiguration | ConvertTo-Json | Set-Content -Path $BluebirdPSConfiguration.ConfigurationPath -Force

        'Saved BluebirdPS Configuration to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.ConfigurationPath | Write-Verbose

    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


function Get-BluebirdPSConfiguration {
    [CmdletBinding()]
    param()

    $BluebirdPSConfiguration
}


function Get-BluebirdPSHistory {
    [CmdletBinding()]
    param(
        [ValidateRange(1,[int]::MaxValue)]
        [int]$First,
        [ValidateRange(1,[int]::MaxValue)]
        [int]$Last,
        [ValidateRange(1,[int]::MaxValue)]
        [int]$Skip,
        [ValidateRange(1,[int]::MaxValue)]
        [int]$SkipLast,
        [switch]$Errors
    )

    $SelectObjectParams = @{}
    foreach ($Key in $PSBoundParameters.Keys) {
        if ($Key -notin [Cmdlet]::CommonParameters -and $Key -ne 'Errors') {
            $SelectObjectParams.Add($Key,$PSBoundParameters[$Key])
        }
    }

    if ($Errors.IsPresent) {
        $SelectObjectParams.Add(
            'Property',
            @(
                'Command',
                'Status'
                @{l='Errors';e= {
                    if ($_.ApiResponse.Errors.Detail) {
                        $_.ApiResponse.Errors.Detail
                    } elseif ($_.ApiResponse.Errors.Message) {
                        $_.ApiResponse.Errors.Message
                    }
                }}
            )
        )
    }

    $BluebirdPSHistoryList | Select-Object @SelectObjectParams
}


function Get-TwitterApiEndpoint {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]$CommandName,
        [ValidateNotNullOrEmpty()]
        [string]$Endpoint
    )

    if ($PSBoundParameters.ContainsKey('Endpoint')) {
        $TwitterEndpoints | Where-Object {$_.ApiEndpoint -match $Endpoint }
    } elseif ($PSBoundParameters.ContainsKey('CommandName')) {
        $TwitterEndpoints | Where-Object {$_.CommandName -in $CommandName}
    } else {
        $TwitterEndpoints
    }

}


function Import-BluebirdPSConfiguration {
    [CmdletBinding()]
    param()

    $FileDescription = 'BluebirdPS configuration file'
    'Checking {0}.' -f $FileDescription | Write-Verbose

    if (Test-Path -Path $BluebirdPSConfiguration.ConfigurationPath) {

        '{0} found.' -f $FileDescription | Write-Verbose

        try {

            'Attempting to import {0}.' -f $FileDescription | Write-Verbose

            $ConfigFromDisk = Get-Content -Path $BluebirdPSConfiguration.ConfigurationPath | ConvertFrom-Json

            # ensure that the configuration file has the correct keys/attributes
            $ConfigObject = [Configuration]@{}

            foreach ($ConfigValue in $ConfigObject.psobject.Properties.Name) {
                if ($ConfigValue -eq 'AuthLastExportDate') {
                    if ($null -ne $ConfigFromDisk.AuthLastExportDate) {
                        $AuthLastExportDate = $ConfigFromDisk.AuthLastExportDate
                        'Importing value {0} into {1}' -f $AuthLastExportDate,$ConfigValue | Write-Verbose
                    } else {
                        if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) {
                            $AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime
                            'Discovered value {0} from LastWriteTime for {1}' -f $AuthLastExportDate,$ConfigValue | Write-Verbose
                        }
                    }
                    $BluebirdPSConfiguration.AuthLastExportDate = $AuthLastExportDate
                    continue
                }

                if ($ConfigValue -in 'RawOutput') {
                    'Configuration value {0} has been removed. Please see documentation for further details.' -f $ConfigValue | Write-Warning
                    continue
                }

                if ($null -ne $ConfigFromDisk.$ConfigValue) {
                    'Importing value {0} into {1}' -f $ConfigFromDisk.$ConfigValue,$ConfigValue | Write-Verbose
                    $BluebirdPSConfiguration.$ConfigValue = $ConfigFromDisk.$ConfigValue
                }

            }

            '{0} imported.' -f $FileDescription | Write-Verbose
        }
        catch {
            '{0} appears to be corrupted. Please run Export-BluebirdPSConfiguration to regenerate.' -f $FileDescription | Write-Warning
        }

    }
}


function Set-BluebirdPSConfiguration {
    [CmdletBinding()]
    param(
        [BluebirdPS.RateLimitAction]$RateLimitAction,
        [int]$RateLimitThreshold,
        [BluebirdPS.OutputType]$OutputType,
        [switch]$Export
    )

    $ConfigParameters = $PSBoundParameters.Keys.Where{
        $_ -notin [Cmdlet]::CommonParameters -and $_ -ne 'Export'
    }

    foreach ($Config in $ConfigParameters) {
        'Setting configuration value for {0} to {1}' -f $Key,$PSBoundParameters[$Config] | Write-Verbose
        $BluebirdPSConfiguration.$Config = $PSBoundParameters[$Config]
    }

    if ($Export.IsPresent) {
        Export-BluebirdPSConfiguration
    } else {
        'Use the -Export switch to save the new configuration to disk.' | Write-Verbose
    }
}


#region Configuration and Authentication
if (-Not (Test-Path -Path $DefaultSavePath)) {

    # on first module import, create default save path and export configuration
    # import authentication will instruct user to run Set-TwiterAuthentication
    New-Item -Path $DefaultSavePath -Force -ItemType Directory | Out-Null
    Export-BluebirdPSConfiguration
    Import-TwitterAuthentication

} else {

    # after first module import, import configuration and authentication
    Import-BluebirdPSConfiguration
    Import-TwitterAuthentication
}
#end region

#region Get-TwitterApiEndpoint setup

# register arugment completers
Register-ArgumentCompleter -CommandName Get-TwitterApiEndpoint -ParameterName CommandName -ScriptBlock {
    param($commandName,$parameterName,$stringMatch) Get-Command -Module BluebirdPS -ListImported | ForEach-Object Name | Where-Object { $_ -match $stringMatch }
}

# store EndpointInfo in module variable
$BluebirdPSCommands = Get-Command -Module BluebirdPS -ListImported

[SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$script:TwitterEndpoints = foreach ($Command in $BluebirdPSCommands) {
    $NavigationLinks = (Get-Help -Name $Command.Name).relatedLinks.navigationLink.Where{$_.linkText -match '^(?!.*(Online|\w+-)).*$'}.Where{$_.linkText -match '- \w+\s(\/|\w+\/)'}
    if ($NavigationLinks.Count -gt 0) {
        $ApiEndpoint = $NavigationLinks.LinkText | ForEach-Object { $_.Split('-')[1].Trim() }
        $ApiDocumentation = $NavigationLinks.Uri
    } else {
        continue
    }
    [EndpointInfo]::new(
        $Command.Name,
        $ApiEndpoint,
        $ApiDocumentation
    )
}
#endregion