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.APIV1
using namespace BluebirdPS.APIV2
using namespace BluebirdPS.APIV2.TweetInfo
using namespace BluebirdPS.APIV2.UserInfo
using namespace BluebirdPS.Exceptions
using namespace BluebirdPS.Validation

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

#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()
$global:BluebirdPSLastResponse = [ResponseData]::new()
#endregion

#region Handle Module Removal
$OnRemoveScript = {
    Remove-Variable -Name BluebirdPSLastResponse -Scope Global -Force
}
$ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript
Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $OnRemoveScript
#endregion Handle Module Removal


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) {
                    { $_ -in 326,453 }                  { 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) }
        MetricsException           { return [MetricsException]::new($ErrorMessage) }
        UnspecifiedException       { return [UnspecifiedException]::new($ErrorMessage) }
        default                    { return [UnspecifiedException]::new($ErrorMessage) }
    }
}


function Invoke-TwitterVerifyCredentials {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [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]@{
            ExpansionType     = 'User'
            IncludeExpansions = $IncludeExpansions
            Endpoint = 'https://api.twitter.com/2/users/me'
        }
    }

    $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
    }

    $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) {
        if ($ApiResponse.errors.message) {
            $ErrorMessage = $ApiResponse.errors.message
        } else {
            $ErrorMessage = $ApiResponse.detail
        }

        $ErrorCategory = Get-ErrorCategory -ErrorType $ApiResponse.Type
        $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory

        if ($ResponseData.Status -eq 403) {
            $ErrorCategory = 'SecurityError'
            $ExceptionType = 'SecurityException'
            $ErrorMessage = "Action forbidden. Please check your permissions. You may need to update your Twitter app's Access Token permissions."
            $IsTerminatingError = $true
        }
        $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) {
            $ErrorParams.Add('ErrorAction','Stop')
        }
        Write-Error @ErrorParams

    } else {
        $TwitterErrors = $ApiResponse.errors
        for ($i = 0; $i -lt $TwitterErrors.Count; $i++) {
            switch ($ResponseData.ApiVersion) {
                1.1 {
                    $ErrorCategory = Get-ErrorCategory -StatusCode $script:LastStatusCode -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
                }
            }

            if ($ErrorMessage -match '_metrics') {
                if ($ErrorMessage -match 'field') {
                    $MetricsRegex = "The '\w+\.\w+' field"
                } else {
                    $MetricsRegex = "'\w+\.\w+'"
                }
                $ExceptionType = 'MetricsException'
                if ($ErrorMessage -match 'organic_metrics') {
                    $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'OrganicMetrics'
                } elseif ($ErrorMessage -match 'promoted_metrics') {
                    $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'PromotedMetrics'
                } if ($ErrorMessage -match 'non_public_metrics') {
                    $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'NonPublicMetrics'
                }
            } else {
                $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 ($ExceptionType -eq 'MetricsException') {
                # only display this exception once despite 3 errors returned for each non_public_metrics field
                $ErrorParams.Add('ErrorAction','Stop')
            } elseif ($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 {

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

        [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
        $global:BluebirdPSLastResponse = $ResponseData

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

        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 {
                        [ResponseInfo]::ParseApiV1Response($ResponseData.ApiResponse)
                    }
                    break
                }
                '2' {
                    if ($ResponseData.ApiResponse.data) {
                        switch ($ResponseData.Command) {
                            'Add-TwitterFriend'             { [ResponseInfo]::UsersFollowingCreateResponse($ResponseData); break }
                            'Remove-TwitterFriend'          { [ResponseInfo]::UsersFollowingDeleteResponse($ResponseData); break }
                            'Set-TwitterBlockedUser'        { [ResponseInfo]::BlockUserMutationResponse($ResponseData); break }
                            'Set-TwitterMutedUser'          { [ResponseInfo]::MuteUserMutationResponse($ResponseData); break }
                            'Set-TweetLike'                 { [ResponseInfo]::SetTweetLikeStatus($ResponseData); break }
                            'Add-TwitterList'               { [ResponseInfo]::ListCreateResponse($ResponseData); break }
                            'Set-TwitterList'               { [ResponseInfo]::ListUpdateResponse($ResponseData); break }
                            'Remove-TwitterList'            { break }
                            'Add-TwitterListMember'         { [ResponseInfo]::ListMutateResponse($ResponseData); break }
                            'Remove-TwitterListMember'      { [ResponseInfo]::ListMutateResponse($ResponseData); break }
                            'Get-TwitterListSubscription'   { [ResponseInfo]::Get2UsersIdFollowedListsResponse($ResponseData); break }
                            'Set-TwitterPinnedList'         { [ResponseInfo]::ListPinnedResponse($ResponseData); break }
                            'Publish-Tweet'                 { $ResponseData.ApiResponse.data; break }
                            default                         { [ResponseInfo]::ParseApiV2Response($ResponseData.ApiResponse); break }
                        }
                        break
                    } else {
                        if ($LastStatusCode -in 403,404) {
                            New-TwitterErrorRecord -ResponseData $ResponseData
                        }
                    }
                }
            }
        }

    }
    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
    )

    if ($RequestParameters.Body -and $RequestParameters.ContentType -eq 'application/json') {
        try {
            $RequestParameters.Body | ConvertFrom-Json -Depth 10 | Out-Null
        }
        catch [Newtonsoft.Json.JsonReaderException] {
            $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
    }

    $VerboseProperties = 'Method','Uri','ContentType','OAuthVersion'
    if ($RequestParameters.Form) {
        $WebRequestParams.Add('Form',$RequestParameters.Form)
        $VerboseProperties  += 'Form'
    } elseif ($RequestParameters.Body) {
        $WebRequestParams.Add('Body',$RequestParameters.Body)
        $VerboseProperties  += 'Body'
    }

    if ($RequestParameters.InvocationInfo.BoundParameters.ContainsKey('Verbose')) {
        [PSCustomObject]($WebRequestParams + @{ OAuthVersion = $RequestParameters.OAuthVersion }) |
            Select-Object -Property $VerboseProperties | Format-List |
            Out-String | Write-Verbose
    }

    try {
        $ApiResponse = Invoke-RestMethod @WebRequestParams
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
    $script:LastStatusCode = $StatusCode
    $script:LastHeaders = $ResponseHeaders

    $ResponseData = [ResponseData]::new($RequestParameters,$Authentication,$ResponseHeaders,$LastStatusCode,$ApiResponse,$BluebirdPSConfiguration.AuthUserName)

    $ShouldResume = $false
    if ($ResponseData.RateLimitRemaining -eq 0) {
        $WaitUntil = New-TimeSpan -End $ResponseData.RateLimitReset
        $RateLimitReached = 'Rate limit of {0} has been reached.' -f $ResponseData.RateLimit,$ResponseData.RateLimitReset
        $RateLimitStop = 'Please wait until {0} before making another attempt for this resource.' -f $ResponseData.RateLimitReset
        $RateLimitWait = 'Waiting until {0} before resuming attempts for this resource.' -f $ResponseData.RateLimitReset

        if ($BluebirdPSConfiguration.RateLimitAction -eq [RateLimitAction]::Resume) {
            $RateLimitReached,$RateLimitWait | Write-Warning
            $ResumeTimeout = $ResponseData.RateLimitReset.AddHours(1)
            $SleepSeconds = 60
            $WriteProgress = @{
                Activity = 'Waiting for RateLimitReset time'
                Status = 'Waiting {0} seconds...' -f $SleepSeconds
            }
            while ($ResponseData.RateLimitReset -gt [datetime]::Now -and [datetime]::Now -lt $ResumeTimeout) {
                $SecondsLeft = $ResponseData.RateLimitReset.Subtract([datetime]::Now).TotalSeconds
                if ($SecondsLeft -le $SleepSeconds) {
                    $SleepSeconds = $SecondsLeft
                }
                $Percent = ($WaitUntil.TotalSeconds - $SecondsLeft) / $WaitUntil.TotalSeconds * 100
                Write-Progress @WriteProgress -SecondsRemaining $SecondsLeft -PercentComplete $Percent
                Start-Sleep -Seconds $SleepSeconds
            }
            Write-Progress @WriteProgress -SecondsRemaining 0 -Completed
            $ShouldResume = $true
        } else {
            $RateLimitReached,$RateLimitStop | 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) {
            [RateLimitAction]::Verbose { $RateLimitMessage | Write-Verbose -Verbose; break}
            [RateLimitAction]::Warning { $RateLimitMessage | Write-Warning -Warning; break}
            [RateLimitAction]::Error { $RateLimitMessage | Write-Error ; break}
            [RateLimitAction]::Resume {
                @($RateLimitMessage,
                    'The RateLimitAction is set to Resume. When the rate limit has been reached, the command will wait until the time above before continuing.'
                ) | Write-Verbose -Verbose
                break
            }
        }
    }

    Write-TwitterResponse -ResponseData $ResponseData

    if ($RequestParameters.NoPagination) {
        return

    } elseif ($ShouldResume) {
        $RequestParameters.Paginate($BluebirdPSLastResponse.ApiResponse.meta.next_token)
        Invoke-TwitterRequest -RequestParameters $RequestParameters

    } else {
        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
                }
                $RequestParameters.Paginate($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
                $RequestParameters.Paginate($ResponseData.ApiResponse.next_cursor)

            } else {
                return
            }

            Write-Progress @Progress
            Start-Sleep -Milliseconds (Get-Random -Minimum 300 -Maximum 600)
            Invoke-TwitterRequest -RequestParameters $RequestParameters
        }
    }
}


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 Show-TwitterAuthentication {
    [OutputType('BluebirdPS.TwitterAuthentication')]
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    param()
    if ($PSCmdlet.ShouldProcess("Twitter Authentication", "Show")) {
        [TwitterAuthentication]::new($OAuth)
    }
}


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 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 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 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 Add-TwitterList {
    [OutputType('BluebirdPS.APIV2.ListInfo.List')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateLength(1,25)]
        [string]$Name,

        [ValidateLength(0,100)]
        [string]$Description,

        [switch]$Private
    )

    $Body = @{
        name = $Name
        private = $Private.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('Description')) {
        $Body.Add('description',$Description)
    }
    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint = 'https://api.twitter.com/2/lists'
        Body = [PSCustomObject]$Body | ConvertTo-Json
    }

    $AddTwitterList = Invoke-TwitterRequest -RequestParameters $Request
    if ($AddTwitterList) {
        Get-TwitterList -Id $AddTwitterList
    }

}


function Add-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [BluebirdPS.APIV2.UserInfo.User[]]$User
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'ById') {
            $ListId = $Id
            $List = Get-TwitterList -Id $Id
        } else {
            $ListId = $List.Id
        }
        if ($List.OwnerId -ne $BluebirdPSConfiguration.AuthUserId) {
            'You must be the owner of a list to add members.' | Write-Error -ErrorAction Stop
        }
    }

    process {
        foreach ($NewMember in $User) {
            if (Test-TwitterListMembership -List $List -User $NewMember) {
                'User {0} is already a member of list {1}' -f $NewMember.Name,$List.ToShortString()
            } else {
                $Request = [TwitterRequest]@{
                    HttpMethod = 'POST'
                    Endpoint ='https://api.twitter.com/2/lists/{0}/members' -f $ListId
                    Body = [PSCustomObject]@{'user_id' = $NewMember.Id } | ConvertTo-Json
                }
                $Request.SetCommandName('Add-TwitterListMember')
                try{
                    $AddTwitterListMember = Invoke-TwitterRequest -RequestParameters $Request
                    if ($AddTwitterListMember) {
                        'User {0} added to list {1}' -f $NewMember.Name,$List.ToShortString()
                    } else {
                        'User {0} was not added to list {1}' -f $NewMember.Name,$List.ToShortString()
                    }
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
        }
    }

}


function Add-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $ListId = $Id
        $List = Get-TwitterList -Id $Id
    } else {
        $ListId = $List.Id
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'POST'
        Endpoint ='https://api.twitter.com/2/users/{0}/followed_lists' -f $BluebirdPSConfiguration.AuthUserId
        Body = [PSCustomObject]@{'list_id' = $ListId } | ConvertTo-Json
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterList {
    [OutputType(
        'BluebirdPS.APIV2.ListInfo.List',
        'BluebirdPS.APIV2.UserInfo.User'
    )]
    [CmdletBinding(DefaultParameterSetName='ByUser')]
    param(
        [Parameter(Mandatory,ParameterSetName='ByListId')]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

        [switch]$IncludeExpansions,

        [ValidateNotNullOrEmpty()]
        [string]$SearchName
    )

    if ($PSCmdlet.ParameterSetName -eq 'ByListId') {
        $Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $Id
    } else {
        $UserId = $User.Id ? $User.Id : $BluebirdPSConfiguration.AuthUserId
        $Endpoint = 'https://api.twitter.com/2/users/{0}/owned_lists' -f $UserId
    }

    $Request = [TwitterRequest]@{
        ExpansionType = 'List'
        Endpoint = $Endpoint
        IncludeExpansions = $IncludeExpansions
    }

    if ($PSBoundParameters.ContainsKey('SearchName')) {
        Invoke-TwitterRequest -RequestParameters $Request | Where-Object Name -match $SearchName
    } else {
        Invoke-TwitterRequest -RequestParameters $Request
    }

}


function Get-TwitterListMember {
    [OutputType(
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.TweetInfo.Tweet'
    )]
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

        [switch]$IncludeExpansions,

        [ValidateRange(1,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    $ListId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $List.Id

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    $Request = [TwitterRequest]@{
        Endpoint ='https://api.twitter.com/2/lists/{0}/members' -f $ListId
        Query = @{'max_results' = $MaxResultsPerPage }
        ExpansionType = 'User'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterListMembership {
    [OutputType(
        'BluebirdPS.APIV2.ListInfo.List',
        'BluebirdPS.APIV2.UserInfo.User'
    )]
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [switch]$IncludeExpansions,

        [ValidateRange(1,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    if ($null -eq $User.Id) {
        $UserId = $BluebirdPSConfiguration.AuthUserId
    } else {
        $UserId = $User.Id
    }
    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    $Request = [TwitterRequest]@{
        Endpoint ='https://api.twitter.com/2/users/{0}/list_memberships' -f $UserId
        Query = @{'max_results' = $MaxResultsPerPage }
        ExpansionType = 'List'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterListSubscriber {
    [OutputType(
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.TweetInfo.Tweet'
    )]
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.ListInfo.List]$List,

        [switch]$IncludeExpansions,

        [ValidateRange(1,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/2/lists/{0}/followers' -f $List.Id
        Query = @{'max_results' = $MaxResultsPerPage }
        ExpansionType = 'User'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterListSubscription {
    [OutputType('BluebirdPS.APIV2.ListInfo.List')]
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [BluebirdPS.APIV2.UserInfo.User]$User,

        [switch]$IncludeExpansions,

        [ValidateRange(1,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    $UserId = $User.Id ? $User.Id : $BluebirdPSConfiguration.AuthUserId
    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    $Request = [TwitterRequest]@{
        Endpoint ='https://api.twitter.com/2/users/{0}/followed_lists' -f $UserId
        Query = @{'max_results' = $MaxResultsPerPage }
        ExpansionType = 'List'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TwitterListTweets {
    [OutputType(
        'BluebirdPS.APIV2.TweetInfo.Tweet',
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.MediaInfo.Media',
        'BluebirdPS.APIV2.Objects.Poll'
    )]
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.ListInfo.List]$List,

        [switch]$IncludeExpansions,

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

        [ValidateRange(10,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/2/lists/{0}/tweets' -f $List.Id
        Query = @{ 'max_results' = $MaxResultsPerPage }
        ExpansionType = 'Tweet'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
        NonPublicMetrics = $NonPublicMetrics
        PromotedMetrics = $PromotedMetrics
        OrganicMetrics = $OrganicMetrics
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterPinnedList {
    [OutputType('BluebirdPS.APIV2.ListInfo.List')]
    [CmdletBinding()]
    param()

    # this endpoint can return pinned lists for any user; this command needs to be updated.
    $Request = [TwitterRequest]@{
        Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists' -f $BluebirdPSConfiguration.AuthUserId
        ExpansionType = 'List'
    }

    Invoke-TwitterRequest -RequestParameters $Request
}


function Remove-TwitterList {
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

    switch ($PSCmdlet.ParameterSetName) {
        'ById' {
            $ListId = $Id
            $List = Get-TwitterList -Id $Id
        }
        'ByList' {
            $ListId = $List.Id
        }
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'DELETE'
        Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $ListId
    }

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

}


function Remove-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [BluebirdPS.APIV2.UserInfo.User[]]$User
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'ById') {
            $ListId = $Id
            $List = Get-TwitterList -Id $Id
        } else {
            $ListId = $List.Id
        }
        if ($List.OwnerId -ne $BluebirdPSConfiguration.AuthUserId) {
            'You must be the owner of a list to remove members.' | Write-Error -ErrorAction Stop
        }
    }

    process {
        foreach ($RemoveMember in $User) {
            if (Test-TwitterListMembership -List $List -User $RemoveMember) {
                $Request = [TwitterRequest]@{
                    HttpMethod = 'DELETE'
                    Endpoint ='https://api.twitter.com/2/lists/{0}/members/{1}' -f $ListId,$RemoveMember.Id
                }
                $Request.SetCommandName('Remove-TwitterListMember')
                try {
                    $RemoveTwitterListMember = Invoke-TwitterRequest -RequestParameters $Request
                    if ($RemoveTwitterListMember) {
                        'User {0} removed from list {1}' -f $RemoveMember.Name,$List.ToShortString()
                    } else {
                        'User {0} was not removed from list {1}' -f $RemoveMember.Name,$List.ToShortString()
                    }
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            } else {
                'User {0} is not a member of list {1}' -f $RemoveMember.Name,$List.ToShortString()
            }
        }
    }

}


function Remove-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(ParameterSetName='ById',Mandatory,ValueFromPipeline)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $ListId = $Id
        $List = Get-TwitterList -Id $Id
    } else {
        $ListId = $List.Id
    }

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

    Invoke-TwitterRequest -RequestParameters $Request

}


function Set-TwitterList {
    [OutputType('BluebirdPS.APIV2.ListInfo.List')]
    [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')]
    param(
        [Parameter(Mandatory,ParameterSetName='ById')]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")]
        [string]$Id,

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

        [Parameter()]
        [ValidateLength(1,25)]
        [string]$Name,

        [ValidateLength(0,100)]
        [string]$Description,

        [boolean]$Private
    )

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $ListId = $Id
        $List = Get-TwitterList -Id $Id
    } else {
        $ListId = $List.Id
    }

    $Body = @{}
    'Name','Description','Private' | ForEach-Object {
        if ($PSBoundParameters.ContainsKey($_)) {
            $Body.Add($_.ToLower(), $PSBoundParameters[$_])
        }
    }

    $Request = [TwitterRequest]@{
        HttpMethod = 'PUT'
        Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $ListId
        Body = [PSCustomObject]$Body | ConvertTo-Json
    }

    try{
        if ($PSCmdlet.ShouldProcess($List.ToString(),'Update list')) {
            $SetTwitterList = Invoke-TwitterRequest -RequestParameters $Request
            if ($SetTwitterList) {
                Get-TwitterList -Id $SetTwitterList
            }
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


function Set-TwitterPinnedList {
    [CmdletBinding(DefaultParameterSetName='PinList')]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.ListInfo.List]$List,

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

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

    if ($PSCmdlet.ParameterSetName -eq 'PinList') {
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists' -f $BluebirdPSConfiguration.AuthUserId
            Body = [PSCustomObject]@{ 'list_id' = $List.Id } | ConvertTo-Json
        }
    } else {
        $Request = [TwitterRequest]@{
            HttpMethod = 'DELETE'
            Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists/{1}' -f $BluebirdPSConfiguration.AuthUserId,$List.Id
        }
    }

    try{
        $SetTwitterPinnedList = Invoke-TwitterRequest -RequestParameters $Request
        $PinnedList = $SetTwitterPinnedList ? 'pinned' : 'not pinned'
        'List {0} is {1}' -f $List.ToShortString(),$PinnedList
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


function Test-TwitterListMembership {
    [OutputType('System.Boolean')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.ListInfo.List]$List,

        [BluebirdPS.APIV2.UserInfo.User]$User
    )

    if (-Not $PSBoundParameters.ContainsKey('User')) {
        $User = Get-TwitterUser
    }

    $UserLists = Get-TwitterListMembership -User $User
    $List.Id -in $UserLists.Id
}


function Test-TwitterListSubscription {
    [OutputType('System.Boolean')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateObjectNotNullOrEmpty()]
        [BluebirdPS.APIV2.ListInfo.List]$List,

        [BluebirdPS.APIV2.UserInfo.User]$User
    )

    if (-Not $PSBoundParameters.ContainsKey('User')) {
        $User = Get-TwitterUser
    }

    $UserSubscriptions = Get-TwitterListSubscription -User $User
    $List.Id -in $UserSubscriptions.Id
}


function Get-Tweet {
    [OutputType(
        'BluebirdPS.APIV2.TweetInfo.Tweet',
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.MediaInfo.Media',
        'BluebirdPS.APIV2.Objects.Poll'
    )]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidateCount(1,100)]
        [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")]
        [string[]]$Id,

        [switch]$IncludeExpansions,

        [ValidateRange(10,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination,

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

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

    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
    }

    Invoke-TwitterRequest -RequestParameters $Request

}


function Get-TweetConversation {
    [OutputType(
        'BluebirdPS.APIV2.TweetInfo.Tweet',
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.MediaInfo.Media',
        'BluebirdPS.APIV2.Objects.Poll'
    )]
    [CmdletBinding(DefaultParameterSetName='ById')]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')]
        [ValidateNotNullOrEmpty()]
        [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,

        [ValidateRange(10,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination,

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

    # The initial Get-Tweet needs to include the switch parameters that are present
    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $Tweet = Get-Tweet -Id $Id
    }

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

    if ($Tweet.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
    }
    $Tweet

    $Request = [TwitterRequest]@{
        Endpoint = 'https://api.twitter.com/2/tweets/search/recent'
        Query = @{ 'query' = ('conversation_id:{0}' -f $Id) }
        ExpansionType = 'Tweet'
        IncludeExpansions = $IncludeExpansions
        NoPagination = $NoPagination
        NonPublicMetrics = $NonPublicMetrics
        PromotedMetrics = $PromotedMetrics
        OrganicMetrics = $OrganicMetrics
    }

    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 = $global:BluebirdPSLastResponse.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 {
    [OutputType(
        'BluebirdPS.APIV2.UserInfo.User'
    )]
    [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,

        [ValidateRange(10,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

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

    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 Publish-Tweet {
    [CmdletBinding(DefaultParameterSetName = 'Tweet')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position = '1')]
        [ValidateLength(1, 10000)]
        [string]$TweetText,

        [Parameter()]
        [string]$ReplyToTweet,

        [Parameter()]
        [string]$QuoteTweet,

        [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
    }

    $Body = @{
        text = $TweetText
    }

    if ($PSBoundParameters.ContainsKey('ReplyToTweet')) {
        $Reply = @{
            in_reply_to_tweet_id = $ReplyToTweet
        }
        $Body.Add('reply', $Reply)
    }

    if ($PSBoundParameters.ContainsKey('QuoteTweet')) {
        $Body.Add('quote_tweet_id', $QuoteTweet)
    }

    if ($MediaId.Count -gt 0) {
        $Media = @{
            media_ids = $MediaId
        }
        $Body.Add('media', $Media)
    }

    $Request = [TwitterRequest]@{
        HttpMethod  = 'POST'
        Endpoint    = 'https://api.twitter.com/2/tweets'
        ContentType = 'application/json'
        Body        = ($Body | Convertto-Json -Depth 10 -Compress)
    }

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


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

        [switch]$IncludeExpansions,

        [ValidateRange(10,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination,

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

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

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

    Invoke-TwitterRequest -RequestParameters $Request
}


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') {
        $Body = @{
            tweet_id = $Id
        }
        $Request = [TwitterRequest]@{
            HttpMethod = 'POST'
            Endpoint   = 'https://api.twitter.com/2/users/{0}/retweets' -f $BluebirdPSConfiguration.AuthUserId
            Body       = ($Body | ConvertTo-Json -Depth 10 -Compress)
        }
    }
    else {
        $Request = [TwitterRequest]@{
            HttpMethod = 'DELETE'
            Endpoint   = 'https://api.twitter.com/2/users/{0}/retweets/{1}' -f $BluebirdPSConfiguration.AuthUserId, $id
        }
    }

    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 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 = 'DELETE'
            Endpoint = 'https://api.twitter.com/2/tweets/{0}' -f $TweetId
        }
        Invoke-TwitterRequest -RequestParameters $Request | Out-Null
    }
}



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
    )

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

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

    try {
        $AddTwitterFriend = Invoke-TwitterRequest -RequestParameters $Request
        $Following = $AddTwitterFriend.following ? 'following' : 'not following'
        'You are {0} user {1}' -f $Following,$User.ToString()
        if ($AddTwitterFriend.pending_follow) {
            'There is a pending follow for user {1}' -f $User.ToString()
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


function Get-TwitterBlockedUser {
    [OutputType('BluebirdPS.APIV2.UserInfo.User')]
    [CmdletBinding()]
    param(
        [switch]$IncludeExpansions,

        [ValidateRange(1,1000)]
        [int]$MaxResultsPerPage=1000,
        [switch]$NoPagination
    )

    if ($MaxResultsPerPage -lt 1000) {
        $NoPagination = $true
    }

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

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterFollowers {
    [OutputType('BluebirdPS.APIV2.UserInfo.User')]
    [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,

        [ValidateRange(1,1000)]
        [int]$MaxResultsPerPage=1000,
        [switch]$NoPagination
    )

    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
        }
    }
    if ($MaxResultsPerPage -lt 1000) {
        $NoPagination = $true
    }

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

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterFriends {
    [OutputType('BluebirdPS.APIV2.UserInfo.User')]
    [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,

        [ValidateRange(1,1000)]
        [int]$MaxResultsPerPage=1000,
        [switch]$NoPagination
    )

    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
        }
    }
    if ($MaxResultsPerPage -lt 1000) {
        $NoPagination = $true
    }

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

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterMutedUser {
    [OutputType('BluebirdPS.APIV2.UserInfo.User')]
    [OutputType('BluebirdPS.APIV2.TweetInfo.Tweet')]
    [CmdletBinding()]
    param(
        [switch]$IncludeExpansions,

        [ValidateRange(1,100)]
        [int]$MaxResultsPerPage=100,
        [switch]$NoPagination
    )

    if ($MaxResultsPerPage -lt 100) {
        $NoPagination = $true
    }

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

    Invoke-TwitterRequest -RequestParameters $Request
}


function Get-TwitterUser {
    [OutputType(
        'BluebirdPS.APIV2.UserInfo.User',
        'BluebirdPS.APIV2.TweetInfo.Tweet'
    )]
    [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/me'
            }
            $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
    )

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $UserId = $Id
        $User = Get-TwitterList -Id $Id
    } else {
        $UserId = $User.Id
    }

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

    try{
        if ($PSCmdlet.ShouldProcess($User.ToString(), 'Unfollow user')) {
            $RemoveTwitterFriend = Invoke-TwitterRequest -RequestParameters $Request
            $Following = $RemoveTwitterFriend ? 'following' : 'not following'
            'You are {0} user {1}' -f $Following,$User.ToString()
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


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
        }
    }

    try{
        $SetTwitterBlockedUser = Invoke-TwitterRequest -RequestParameters $Request
        $Blocking = $SetTwitterBlockedUser ? 'blocking' : 'not blocking'
        'You are {0} user {1}' -f $Blocking,$User.ToString()
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}


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]@{}
    if ($PSCmdlet.ParameterSetName -eq 'Mute') {
        $Request.HttpMethod = 'POST'
        $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/muting' -f $BluebirdPSConfiguration.AuthUserId
        $Request.Body = [PSCustomObject]@{ 'target_user_id' = $User.Id } | ConvertTo-Json
    } else {
        $Request.HttpMethod = 'DELETE'
        $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/muting/{1}' -f $BluebirdPSConfiguration.AuthUserId,$User.Id
    }

    try {
        $SetTwitterMutedUser = Invoke-TwitterRequest -RequestParameters $Request
        $Muting = $SetTwitterMutedUser ? 'muting' : 'not muting'
        'You are {0} user {1}' -f $Muting,$User.ToString()
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}


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 Find-TwitterMastodonLinks {
    [OutputType('BluebirdPS.TwitterMastodonReference')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [BluebirdPS.TwitterObject[]]$TwitterObject,
        [ValidateNotNullOrEmpty()]
        [Alias('IgnoreUrl')]
        [string[]]$IgnoreDomain
    )
    begin {
        $MastodonUserAccountRegex = '(?<MastodonUser>@\S+)@(?<MastodonInstance>[\w-.]+)'
        $MastodonUserUrlRegex = '(?<MastodonInstance>[\w-.]+\w+(\/web)*)\/(?<MastodonUser>@[\w+]+)'
        $MastodonUsers = [System.Collections.Generic.List[TwitterMastodonReference]]::new()
        $TwitterUsers =  [System.Collections.Generic.List[User]]::new()
        $Tweets =  [System.Collections.Generic.List[Tweet]]::new()
        if ($PSBoundParameters.ContainsKey('IgnoreDomain')) {
            $IgnoreDomainRegex = $IgnoreDomain -join '|'
        } else {
            $IgnoreDomainRegex = $false
        }
    }
    process {
        foreach ($Object in $TwitterObject) {
            if ($Object.GetType() -match 'User') {
                $TwitterUsers.Add($Object)
            }
            if ($Object.GetType() -match 'Tweet') {
                $Tweets.Add($Object)
            }
        }
    }
    end {
        foreach ($User in $TwitterUsers) {
            if ($User.Name -match  $MastodonUserAccountRegex) {
                if ($IgnoreDomainRegex) {
                    if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) {
                        continue
                    }
                }
                $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'Name')
                if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) {
                    $MastodonUsers.Add($MastodonUser)
                }
            }
            if ($User.Description -match  $MastodonUserAccountRegex) {
                if ($IgnoreDomainRegex) {
                    if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) {
                        continue
                    }
                }
                $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'Description')
                if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) {
                    $MastodonUsers.Add($MastodonUser)
                }
            }
            foreach ($Url in ($User.Entities.Where{$_.GetType() -match 'Url'} )) {
                if ($Url.ToString() -match  $MastodonUserUrlRegex) {
                    if ($IgnoreDomainRegex) {
                        if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) {
                            continue
                        }
                    }
                    $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'UrlEntity')
                    if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) {
                        $MastodonUsers.Add($MastodonUser)
                    }
                }
            }
        }
        foreach ($Tweet in $Tweets) {
            $User = $TwitterUsers | Where-Object Id -eq $Tweet.AuthorId | Select-Object -First 1
            if ($Tweet.Text -match $MastodonUserAccountRegex) {
                if ($IgnoreDomainRegex) {
                    if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) {
                        continue
                    }
                }
                $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'TweetText')
                if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) {
                    $MastodonUsers.Add($MastodonUser)
                }
            }
            foreach ($Url in ($Tweet.Entities.Where{$_.GetType() -match 'Url'})) {
                if ($Url.ToString() -match  $MastodonUserUrlRegex) {
                    if ($IgnoreDomainRegex) {
                        if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) {
                            continue
                        }
                    }
                    $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'TweetUrlEntity')
                    if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) {
                        $MastodonUsers.Add($MastodonUser)
                    }
                }
            }
            $User = $null
        }
        $MastodonUsers | Sort-Object -Property TwitterUserName
    }
}


function Get-BluebirdPSAssemblyDetails {
    [OutputType('System.Reflection.TypeInfo')]
    param()
    ([System.AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object {$_.Location -match 'bluebirdps'}).GetTypes() |
        Where-Object {$_.Namespace -and $_.Fullname -notmatch '\+'} |
        Sort-Object -Property Namespace,Fullname
}


function Get-BluebirdPSConfiguration {
    [OutputType('BluebirdPS.Configuration')]
    [CmdletBinding()]
    param()
    $BluebirdPSConfiguration
}


function Get-BluebirdPSHistory {
    [OutputType('BluebirdPS.ResponseData')]
    [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-BluebirdPSVersion {
    [CmdletBinding()]
    param()
    $BluebirdPSVersion
}


function Get-TwitterApiEndpoint {
    [OutputType('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
                }

                # Deprecated configuration properties that were not moved to BluebirdPS Profile
                if ($ConfigValue -in 'RawOutput') {
                    'Configuration property {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
                }
            }

            $BluebirdPSRateLimitAction = 'env:BLUEBIRDPS_RATE_LIMIT_ACTION'
            if (Test-Path -Path $BluebirdPSRateLimitAction) {
                if ($env:BLUEBIRDPS_RATE_LIMIT_ACTION -in [enum]::GetNames([BluebirdPS.RateLimitAction])) {
                    if ($BluebirdPSConfiguration.RateLimitAction -eq $env:BLUEBIRDPS_RATE_LIMIT_ACTION) {
                        'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. The value {0} is the same as the currently saved value.' -f $BluebirdPSConfiguration.RateLimitAction  | Write-Verbose
                    } else {
                        'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. Overriding RateLimitAction value: {0} (current), {1} (override).' -f $BluebirdPSConfiguration.RateLimitAction,$env:BLUEBIRDPS_RATE_LIMIT_ACTION  | Write-Verbose
                        $BluebirdPSConfiguration.RateLimitAction = $env:BLUEBIRDPS_RATE_LIMIT_ACTION
                    }
                } else {
                    'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. The value {0} is not valid.' -f $env:BLUEBIRDPS_RATE_LIMIT_ACTION | Write-Warning
                }
            }

            '{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

#region BluebirdPS Version
$ModuleManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'BluebirdPS.psd1'
[SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$BluebirdPSVersion = (Import-PowerShellDataFile -Path $ModuleManifestPath).ModuleVersion
#endregion