BluebirdPS.psm1

using namespace System.Management.Automation
using namespace Collections.ObjectModel
using namespace System.Collections
using namespace Microsoft.PowerShell.Commands

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

#region set base path variables
$ResourcesPath = Join-Path -Path $PSScriptRoot -ChildPath 'resources'
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
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$OAuthTokenSavePath = Join-Path -Path $DefaultSavePath -ChildPath 'twittercred.sav'

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$OAuth =  @{
    ApiKey = $null
    ApiSecret = $null
    AccessToken = $null
    AccessTokenSecret = $null
    BearerToken = $null
}
#endregion

#region Configuration variables and setup
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$ConfigurationSavePath = Join-Path -Path $DefaultSavePath -ChildPath 'TwitterConfiguration.json'

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$TwitterConfiguration = $null

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$TwitterConfigurationRefreshDate = $null
#endregion

#region
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$LanguagesSavePath = Join-Path -Path $DefaultSavePath -ChildPath 'TwitterLanguages.json'

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$TwitterLanguages = $null
#endregion

#region ErrorMapping variables and setup
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$ErrorMappingPath = Join-Path -Path $ResourcesPath -ChildPath 'TwitterErrorCodeExceptions.json'

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$TwitterErrorMapping = $null
#endregion

#region other variables
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$ApiEndpointsPath = Join-Path -Path $ResourcesPath -ChildPath 'TwitterApiEndpoints.json'

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$RateLimitWarning = $false

[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$TwitterHistoryList = [System.Collections.Generic.List[object]]::new()
#endregion
#region source: \classes\OAuthParameters.Class.psm1
class OAuthParameters {
    [string]$HttpMethod
    [String]$BaseUri
    [hashtable]$Query
    [System.UriBuilder]$UriBuilder
    [string]$UnescapedQueryString
    [string]$EscapedQueryString
    [object]$Body
    [hashtable]$Form

    # ------------------------------------------------------------------------------------------------------------------
    # constructor for OAuthParameters
    # ------------------------------------------------------------------------------------------------------------------

    OAuthParameters ([string] $HttpMethod, [string] $BaseUri, [hashtable] $Query) {
        $this.SetHttpMethod($HttpMethod)
        $this.SetBaseUri($BaseUri)
        $this.SetQuery($Query)
    }

    OAuthParameters ([string] $HttpMethod, [string] $BaseUri) {
        $this.SetHttpMethod($HttpMethod)
        $this.SetBaseUri($BaseUri)
    }

    # ------------------------------------------------------------------------------------------------------------------
    # methods
    # ------------------------------------------------------------------------------------------------------------------

    # generates the required OAuth signature string
    # Nonce and Timestamp are variables in order to support testing
    [string] GetOAuthSignatureString (
        [string]$ApiKey,[string]$ApiSecret,
        [string]$AccessToken,[string]$AccessTokenSecret,
        [string]$Nonce,[string]$Timestamp
    ) {
        if ([System.String]::IsNullOrEmpty($Nonce)) {
            $Nonce = $this.GetNonce()
        }
        if ([System.String]::IsNullOrEmpty($Timestamp)) {
            $Timestamp = $this.GetUnixTime()
        }

        # get parameter string
        $ParameterString = $this.GetParameterString($ApiKey,$AccessToken,$Nonce,$Timestamp)

        # create signature base
        $SignatureBaseString = '{0}&{1}&{2}' -f $this.HttpMethod,
            [System.Uri]::EscapeDataString($this.BaseUri),
            [System.Uri]::EscapeDataString($ParameterString)

        # set signing key
        $SigningKey = '{0}&{1}' -f [System.Uri]::EscapeDataString($ApiSecret),
            [System.Uri]::EscapeDataString($AccessTokenSecret)

        # generate OAuth signature
        $SignatureMethod = [System.Security.Cryptography.HMACSHA1]::new()
        $SignatureMethod.Key = [System.Text.Encoding]::ASCII.GetBytes($SigningKey)
        $OAuthSignature = [System.Convert]::ToBase64String(
            $SignatureMethod.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($SignatureBaseString))
        )

        # add OAuth signature to header
        $OAuthHeader = $this.GetOAuthHeader($ApiKey,$AccessToken,$Nonce,$Timestamp)
        $OAuthHeader.Add('oauth_signature',$OAuthSignature)

        # escape OAuth key
        $OAuthHeaderString = $OAuthHeader.GetEnumerator() |
            Sort-Object Name |
            ForEach-Object {
                '{0}="{1}"' -f $_.Key,[System.Uri]::EscapeDataString($_.Value)
            }

        # build OAuth sstring
        $OAuthString = [System.Text.StringBuilder]::new()
        $null = $OAuthString.Append('OAuth ')
        $null = $OAuthString.Append(($OAuthHeaderString -join ', '))

        return $OAuthString.ToString()
    }

    # used to update Query, UnescapedQueryString, and UriBuilder
    # required for cursoring and paging
    [void] SetQuery($Query) {
        $this.Query = $Query
        $UriQueryString = [System.Collections.Generic.List[string]]::new()
        $this.Query.Keys | Sort-Object | Foreach-Object {
            $null = $UriQueryString.Add(('{0}={1}' -f $_,$this.Query[$_]))
        }
        $this.UnescapedQueryString = $UriQueryString -join '&'
        $this.UriBuilder = ($null -ne $this.Query) ? '{0}?{1}' -f $this.BaseUri,$this.UnescapedQueryString : $this.BaseUri
        $this.EscapedQueryString = $this.UriBuilder.Uri.Query.Substring(1)
    }

    # provide simple way to include text output
    [string] ToString() {

        $QueryObject = [PsCustomObject][ordered]@{}
        foreach ($Key in ($this.Query.Keys | Sort-Object)) {
            $QueryObject | Add-Member -MemberType NoteProperty -Name $Key -Value $this.Query[$Key]
        }

        $OAuthParametersString = [PsCustomObject]@{
            HttpMethod = $this.HttpMethod
            BaseUri = $this.BaseUri
            Query = ($QueryObject | Format-List | Out-String).Trim()
            Uri = $this.UriBuilder.Uri.AbsoluteUri
            UnescapedQueryString = $this.UnescapedQueryString
            EscapedQueryString = $this.EscapedQueryString
            Body = $this.Body
            Form = $this.Form
        }

        return $OAuthParametersString | Format-List | Out-String
    }

    # ------------------------------------------------------------------------------------------------------------------
    # hidden methods
    # note - only visible when using 'Get-Member -Force'
    # - the setter and getter methods for the class properties will be revealed with -Force
    # ------------------------------------------------------------------------------------------------------------------

    # generate random base64 encoding 32 bytes
    hidden [string] GetNonce() {
        $NumberBytes = [Byte[]]::new(32)
        $Random = [System.Security.Cryptography.RandomNumberGenerator]::Create()
        $Random.GetBytes($NumberBytes)
        $Random.Dispose()
        return [System.Convert]::ToBase64String($NumberBytes)
    }

    # generate UNIX epoch time
    hidden [long] GetUnixTime(){
        return [long]([datetime]::Now - [DateTime]::new(1970,1,1,0,0,0,[DateTimeKind]::Utc)).TotalSeconds
    }

    # create the base OAuth Headers
    hidden [hashtable] GetOAuthHeader($ApiKey,$AccessToken,$Nonce,$Timestamp) {
        return @{
            oauth_consumer_key = $ApiKey
            oauth_nonce = $Nonce
            oauth_signature_method = 'HMAC-SHA1'
            oauth_timestamp = $Timestamp
            oauth_token = $AccessToken
            oauth_version = '1.0'
        }
    }

    hidden [string] GetParameterString($ApiKey,$AccessToken,$Nonce,$Timestamp) {

        # get OAuthHeader
        $OAuthHeader = $this.GetOAuthHeader($ApiKey,$AccessToken,$Nonce,$Timestamp)

        # collect parameters
        $Parameters = @{}
        foreach ($Key in $this.Query.Keys) {
            $Parameters.Add($Key,$this.Query[$Key])
        }
        <#foreach ($Key in $this.Query.Keys) {
            if ([System.Uri]::UnescapeDataString($this.Query[$Key]) -eq $this.Query[$Key]) {
                $ParamValue = [System.Uri]::EscapeDataString($this.Query[$Key])
            } else {
                $ParamValue = $this.Query[$Key]
            }
            $Parameters.Add(
                [System.Uri]::EscapeDataString($Key),
                $ParamValue
                #[System.Uri]::EscapeDataString($ParamValue)
            )
        }#>


        # append OAuth header
        foreach ($Key in $OAuthHeader.Keys) {
            $Parameters.Add($Key,$OAuthHeader[$Key])
        }

        # build parameter string, esnure that every key and value have been escaped only once
        $ParameterString = $Parameters.GetEnumerator() |
            Sort-Object -Property Name |
            ForEach-Object {
                $EscapedKey = ([System.Uri]::UnescapeDataString($_.Key) -eq $_.Key) ? [System.Uri]::EscapeDataString($_.Key) : $_.Key
                $EscapedValue = ([System.Uri]::UnescapeDataString($_.Value) -eq $_.Value) ? [System.Uri]::EscapeDataString($_.Value) : $_.Value
                '{0}={1}' -f $EscapedKey, $EscapedValue
            }

        return $ParameterString -join '&'
    }

    hidden [void] SetHttpMethod($HttpMethod) {
        if ($HttpMethod -match '\bGET\b|\bPOST\b|\bDELETE\b|\bPATCH\b|\bPUT\b|\bHEAD\b|\bOPTIONS\b|\bTRACE\b') {
            $this.HttpMethod = $HttpMethod.ToUpper()
        } else {
            throw "The value supplied for HttpMethod [$HttpMethod] is not a valid HTTP method."
        }
    }

    # maintain base uri separately as it will be used in GetOAuthSignatureString() method and in calling function
    hidden [void] SetBaseUri($BaseUri) {
        $this.BaseUri = $BaseUri
        $this.UriBuilder = $this.BaseUri
    }

}
#endregion

#region source: \private\Get-SendMediaStatus.ps1
function Get-SendMediaStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias('media_id')]
        [string]$MediaId,

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

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://upload.twitter.com/1.1/media/upload.json',
        @{
            '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 -OAuthParameters $OAuthParameters
            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 -OAuthParameters $OAuthParameters
    }

}

#endregion

#region source: \private\Import-TwitterResource.ps1
function Import-TwitterResource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='Configuration')]
        [switch]$Configuration,
        [Parameter(Mandatory,ParameterSetName='Languages')]
        [switch]$Languages,
        [Parameter(Mandatory,ParameterSetName='ErrorMapping')]
        [switch]$ErrorMapping
    )

    switch ($PSCmdlet.ParameterSetName) {
        'Configuration' {
            $ResourcePath = $ConfigurationSavePath
        }
        'Languages' {
            $ResourcePath = $LanguagesSavePath
        }
        'ErrorMapping' {
            $ResourcePath = $ErrorMappingPath
        }
    }

    'Importing {0} from {1}' -f $PSCmdlet.ParameterSetName,$ResourcePath | Write-Verbose
    try {
        if (Test-Path -Path $ResourcePath) {
            $Resource = Get-Content -Path $ResourcePath -Raw | ConvertFrom-Json
        } else {
            if ($PSCmdlet.ParameterSetName -ne 'ErrorMapping') {
                $ErrorMessage = '{0}Unable to find {1} resource data.{0}Please export the resource using "Export-TwitterResource -{1}"' -f [System.Environment]::NewLine,$PSCmdlet.ParameterSetName
                $ErrorMessage | Write-Warning
                return
            }
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    switch ($PSCmdlet.ParameterSetName) {
        'Configuration' {
            [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
            $script:TwitterConfiguration = $Resource
        }
        'Languages' {
            [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
            $script:TwitterLanguages = $Resource
        }
        'ErrorMapping' {
            [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
            $script:TwitterErrorMapping = $Resource
        }
    }

}

#endregion

#region source: \private\Invoke-TwitterCursorRequest.ps1
function Invoke-TwitterCursorRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [OAuthParameters]$OAuthParameters,
        [string]$ReturnValue
    )

    $NextCursor = -1
    do {

        if ($OAuthParameters.BaseUri -eq 'https://api.twitter.com/1.1/direct_messages/events/list.json' -and $NextCursor -eq -1) {
            # Get-TwitterDM
            # do not send a cursor for the first API call
        } else {
            if ($OAuthParameters.Query.Keys -match 'cursor') {
                $OAuthParameters.Query.Remove('cursor')
            }
            $OAuthParameters.Query.Add('cursor',$NextCursor)
            $OAuthParameters.SetQuery($OAuthParameters.Query)
        }

        try {
            $TwitterRequest = Invoke-TwitterRequest -OAuthParameters $OAuthParameters
            if ($TwitterRequest -is [System.Management.Automation.ErrorRecord]) {
                $PSCmdlet.ThrowTerminatingError($TwitterRequest)
            }

            if ($TwitterRequest.psobject.Properties.Name -contains $ReturnValue) {
                $TwitterRequest.$ReturnValue
            } else {
                $TwitterRequest
            }

            $NextCursor = $TwitterRequest.next_cursor

            # Get-TwitterDM
            if ($null -eq $NextCursor) {
                break
            }
        }
        catch {
            $PSCmdlet.WriteError($_)
            break
        }

    } while ($NextCursor -ne 0)
}

#endregion

#region source: \private\Invoke-TwitterPageRequest.ps1
function Invoke-TwitterPageRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [OAuthParameters]$OAuthParameters,
        [Parameter(Mandatory)]
        [int]$Pages
    )

    'Incoming OAuthParameters:',$OAuthParameters | Write-Verbose

    $CurrentCount = 0
    $AllPages = @()

    for ($CurrentPage = 1 ; $CurrentPage -le $Pages; $CurrentPage++) {

        if ($OAuthParameters.Query.Keys -match 'page') {
            $OAuthParameters.Query.Remove('page')
        }
        $OAuthParameters.Query.Add('page',$CurrentPage)

        if ($CurrentPage -eq $Pages) {
            $Count = $MaxResults - $CurrentCount
        } else {
            $Count = 20
        }
        if ($OAuthParameters.Query.Keys -match 'count') {
            $OAuthParameters.Query.Remove('count')
        }
        $OAuthParameters.Query.Add('count',$Count)

        $OAuthParameters.SetQuery($OAuthParameters.Query)
        'OAuthParameters:',$OAuthParameters | Write-Verbose

        try {
            $PageRequest = Invoke-TwitterRequest -OAuthParameters $OAuthParameters
            $AllPages = $AllPages + $PageRequest
            $CurrentCount = $CurrentCount + $Count
        }
        catch {
            $PSCmdlet.WriteError($_)
            break
        }
    }

    $AllPages
}
#endregion

#region source: \private\Invoke-TwitterRequest.ps1
function Invoke-TwitterRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [OAuthParameters]$OAuthParameters,
        [switch]$SkipHistory
    )

    try {
        $OAuthHeaderString = $OAuthParameters.GetOAuthSignatureString(
            $OAuth['ApiKey'],$OAuth['ApiSecret'],
            $OAuth['AccessToken'],$OAuth['AccessTokenSecret'],
            $null,$null
        )
        '{0}OAuthHeaderString:{0}{1}' -f [System.Environment]::NewLine,$OAuthHeaderString | Write-Verbose

        $WebRequestParams = @{
            Uri = $OAuthParameters.UriBuilder.Uri.AbsoluteUri
            Method = $OAuthParameters.HttpMethod
            Headers = @{ 'Authorization' = $OAuthHeaderString }
            ContentType = 'application/json'
            ResponseHeadersVariable = 'TwitterResponse'
            StatusCodeVariable = 'TwitterStatusCode'
            Verbose = $false
        }
        if ($OAuthParameters.Form) {
            $WebRequestParams.Add('Form',$OAuthParameters.Form)
        } elseif ($OAuthParameters.Body) {
            $WebRequestParams.Add('Body',$OAuthParameters.Body)
        }
        '{0}WebRequest Parameters:{0}{1}{0}' -f [System.Environment]::NewLine,($WebRequestParams.GetEnumerator() | Out-String).Trim() | Write-Verbose

        Invoke-RestMethod @WebRequestParams

        $ResponseData = [PsCustomObject]@{
            TwitterResponse = $TwitterResponse
            StatusCode = $TwitterStatusCode
            Uri = $OAuthParameters.BaseUri
            QueryString = $OAuthParameters.UriBuilder.Uri.Query
            HttpMethod = $OAuthParameters.HttpMethod
        }
        if ($PSBoundParameters.ContainsKey('SkipHistory')) {
            $ResponseData | Write-TwitterResponseData -SkipHistory
        } else {
            $ResponseData | Write-TwitterResponseData
        }

    }
    catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        $_ | New-TwitterErrorRecord
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}
#endregion

#region source: \private\New-TwitterErrorRecord.ps1
function New-TwitterErrorRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ErrorRecord]$ErrorRecord
    )

    try {

        $Command = (Get-PSCallStack).Where{$_.Command -notmatch 'ErrorRecord|ResponseData|Request|ScriptBlock'}.Command

        $Response = $ErrorRecord.Exception.Response
        if ($Response.Headers.ToString() -match '(?:x-response-time:\s)(\d+)') {
            $ResponseTime = $Matches[1]
        }

        if ($Response.RequestMessage.RequestUri.Query.Length -gt 0) {
            $Uri = $Response.RequestMessage.RequestUri.AbsoluteUri.Replace($Response.RequestMessage.RequestUri.Query,'')
        } else {
            $Uri = $Response.RequestMessage.RequestUri.AbsoluteUri
        }

        $Status = '{0} {1}' -f $Response.StatusCode.Value__,$Response.ReasonPhrase
        $Server = $Response.Headers.Where{$_.Key -eq 'server'}.Value

        try {
            if ($ErrorRecord.ErrorDetails) {
                $TwitterError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).errors
                $ErrorCategory = $TwitterErrorMapping.Where{$_.ErrorCode -eq $TwitterError.code}.Exception
                $ErrorMessage = $TwitterError.message
                $ErrorCode = $TwitterError.code
            } else {
                $MappedError = $TwitterErrorMapping.Where{$_.HttpStatusCode -eq $Response.StatusCode.Value__}
                $ErrorCategory = $MappedError.Exception
                if ($MappedError.Message) {
                    $ErrorMessage = $MappedError.Message
                } else {
                    $ErrorMessage = $TwitterError.ReasonPhrase
                }
                $ErrorCode = $null
            }
        }
        catch {
            $ErrorCategory = $ErrorRecord.CategoryInfo.Category
            $ErrorMessage = $Response.StatusCode
            $ErrorCode = $null
        }

        $ResponseData = [PsCustomObject]@{
            PSTypeName = 'Twitter.Error'
            Command = $Command
            HttpMethod = $Response.RequestMessage.Method.ToString()
            Uri = $Uri
            QueryString = $Response.RequestMessage.RequestUri.Query
            Status = $Status
            Message = $ErrorMessage
            Server = $Server
            ResponseTime = $ResponseTime
            Response = $Response
            ErrorCode = $ErrorCode
        }
        $TwitterHistoryList.Add($ResponseData)
        Write-Information -MessageData $ResponseData

        $TwitterException = [HttpResponseException]::new($ErrorMessage,$Response)

        [ErrorRecord]::new($TwitterException,$Command,$ErrorCategory,$ResponseData.Uri)

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

}
#endregion

#region source: \private\New-TwitterQuery.ps1
function New-TwitterQuery {
    [CmdletBinding()]
    param(
        [hashtable]$ApiParameters
    )

    $TwitterQuery = [hashtable]::new()

    foreach ($Parameter in $ApiParameters.Keys) {
        switch ($Parameter) {
            'ScreenName' {
                # GET friendships/lookup allows for multiple input
                if ($ApiParameters[$Parameter] -is [array]) {
                    $TwitterQuery.Add('screen_name',($ApiParameters[$Parameter] -join ','))
                } else {
                    $TwitterQuery.Add('screen_name',$ApiParameters[$Parameter])
                }
            }
            'UserId' {
                # GET friendships/lookup allows for multiple input
                if ($ApiParameters[$Parameter] -is [array]) {
                    $TwitterQuery.Add('user_id',($ApiParameters[$Parameter] -join ','))
                } else {
                    $TwitterQuery.Add('user_id',$ApiParameters[$Parameter])
                }
            }
            'Count' {
               $TwitterQuery.Add('count',$ApiParameters[$Parameter])
            }
            'TweetId' {
                $TwitterQuery.Add('id',$ApiParameters[$Parameter])
            }
            'SinceId' {
                $TwitterQuery.Add('since_id',$ApiParameters[$Parameter])
            }
            'MaxId' {
                $TwitterQuery.Add('max_id',$ApiParameters[$Parameter])
            }
            'SkipStatus' {
                $TwitterQuery.Add('skip_status',$ApiParameters[$Parameter])
            }
            'ResultsPerPage' {
                $TwitterQuery.Add('count',$ApiParameters[$Parameter])
            }
            'SourceScreenName' {
                $TwitterQuery.Add('source_screen_name',$ApiParameters[$Parameter])
            }
            'SourceUserId' {
                $TwitterQuery.Add('source_id',$ApiParameters[$Parameter])
            }
            'TargetScreenName' {
                $TwitterQuery.Add('target_screen_name',$ApiParameters[$Parameter])
            }
            'TargetUserId' {
                $TwitterQuery.Add('target_id',$ApiParameters[$Parameter])
            }
            'Slug' {
                $TwitterQuery.Add('slug',$ApiParameters[$Parameter])
            }
            'OwnerScreenName' {
                $TwitterQuery.Add('owner_screen_name',$ApiParameters[$Parameter])
            }
            'OwnerId' {
                $TwitterQuery.Add('owner_id',$ApiParameters[$Parameter])
            }
            'ListId' {
                $TwitterQuery.Add('list_id',$ApiParameters[$Parameter])
            }
            'OwnedListFirst' {
                $TwitterQuery.Add('reverse','true')
            }
            'ExcludeRetweets' {
                $TwitterQuery.Add('include_rts','false')
            }
            'SearchString' {
                $TwitterQuery.Add('q',[System.Uri]::EscapeDataString($ApiParameters[$Parameter]))
            }
            'MaxResults' {
                $TwitterQuery.Add('count',$ApiParameters[$Parameter])
            }
            'TweetMode' {
                $TweetModeValue = $ApiParameters[$Parameter] -eq 'Compatibility' ? 'compat' : 'extended'
                $TwitterQuery.Add('tweet_mode',$TweetModeValue)
            }
        }
    }

    if ($ApiParameters.ContainsKey('ExcludeEntities')) {
        $TwitterQuery.Add('include_entities','false')
    } else {
        $TwitterQuery.Add('include_entities','true')
    }

    '{0}Twitter Query:{0}{1}' -f [System.Environment]::NewLine,($TwitterQuery.GetEnumerator() | Out-String).Trim() | Write-Verbose
    $TwitterQuery
}

#endregion

#region source: \private\New-ValidationErrorRecord.ps1
function New-ValidationErrorRecord {
    [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
    )
}
#endregion

#region source: \private\Set-TwitterMediaAltImageText.ps1
function Set-TwitterMediaAltImageText {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias('media_id')]
        [string]$MediaId,

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

    $OAuthParameters = [OAuthParameters]::new(
        'POST',
        'https://upload.twitter.com/1.1/media/metadata/create.json'
    )

    $OAuthParameters.Body = ('{{"media_id":"{0}","alt_text":{{"text":"{1}"}}}}' -f $MediaId,$AltImageText)
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}
#endregion

#region source: \private\Write-TwitterResponseData.ps1
function Write-TwitterResponseData {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [object]$TwitterResponse,
        [Parameter(ValueFromPipelineByPropertyName)]
        [int32]$StatusCode,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Uri,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$HttpMethod,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$QueryString,
        [switch]$SkipHistory
    )

    try {

        $Command = (Get-PSCallStack).Where{$_.Command -notmatch 'TwitterResource|ErrorRecord|ResponseData|Request|ScriptBlock'}.Command
        if ($Command -is [array]) {
            $Command = $Command[0]
        }
        if ($TwitterResponse.Status) {
            $Status = $TwitterResponse.Status[0]
        }
        if ($TwitterResponse.Server) {
            $Server = $TwitterResponse.Server[0]
        }
        if ($TwitterResponse['x-response-time']) {
            $ResponseTime = $TwitterResponse['x-response-time'][0]
        }

        $ResponseData = [PsCustomObject]@{
            PSTypeName = 'Twitter.Response'
            Command = $Command
            HttpMethod = $HttpMethod
            Uri = $Uri
            QueryString = $QueryString
            Status = $Status
            Server = $Server
            ResponseTime = $ResponseTime
            RateLimit = $null
            RateLimitRemaining = $null
            RateLimitReset = $null
            Response = $TwitterResponse
        }
        if ($TwitterResponse['x-rate-limit-limit']) {
            [int]$ResponseData.RateLimit = $TwitterResponse['x-rate-limit-limit'][0]
        }
        if ($TwitterResponse['x-rate-limit-remaining']) {
            [int]$ResponseData.RateLimitRemaining = $TwitterResponse['x-rate-limit-remaining'][0]
        }
        if ($TwitterResponse['x-rate-limit-reset']) {
            [long]$Ticks = $TwitterResponse['x-rate-limit-reset'][0]
            $ResponseData.RateLimitReset = (Get-Date -Date '1/1/1970').AddSeconds($Ticks).ToLocalTime()
        }

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

        if (($ResponseData.RateLimitRemaining -le 5 -and $null -ne $ResponseData.RateLimitRemaining) -or $RateLimitWarning) {
            $RateWarningMessage = '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
            $RateWarningMessage | Write-Warning
        }

        if (-Not $PSBoundParameters.ContainsKey('SkipHistory')) {
            $TwitterHistoryList.Add($ResponseData)
        }
        Write-Information -MessageData $ResponseData
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}

#endregion

#region source: \public\api\authentication\Export-TwitterAuthentication.ps1
function Export-TwitterAuthentication {
    [CmdletBinding()]
    param()

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

        $OAuth | ConvertTo-Json | ConvertTo-SecureString -AsPlainText |
            ConvertFrom-SecureString | Set-Content -Path $OAuthTokenSavePath -Force

        'Saved Twitter credentials to {0} file: {1}' -f $Action,$OAuthTokenSavePath | Write-Verbose
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}

#endregion

#region source: \public\api\authentication\Import-TwitterAuthentication.ps1
function Import-TwitterAuthentication {
    [CmdletBinding()]
    param()

    'Checking Twitter credentials file.' | Write-Verbose

    $SetValues = 'Please use Set-TwitterAuthentication to set the requried API keys and secrets. Use the -Persist switch to save the values to disk.'

    if (Test-Path -Path $OAuthTokenSavePath) {

        'Twitter credentials file found.' | Write-Verbose

        try {

            'Attempting to import Twitter credentials file.' | Write-Verbose

            # read the encrypted credentials file, decrypt, and convert from JSON to object
            $OAuthFromDisk = Get-Content -Path $OAuthTokenSavePath | 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
                $OAuth['BearerToken'] = $OAuthFromDisk.BearerToken

                'Twitter credentials file imported.' | Write-Verbose

            } else {
                'Authentication file missing one or more values. {0}' -f $SetValues | Write-Warning
            }
        }
        catch {
            'Authentication file appears to be corrupted. {0}' -f $SetValues | Write-Warning
        }

    } else {
        $SetValues | Write-Warning
    }
}

#endregion

#region source: \public\api\authentication\Set-TwitterAuthentication.ps1
function Set-TwitterAuthentication {
    [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),
        [switch]$Persist
    )

    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

            if ($PSBoundParameters.ContainsKey('Persist')) {
                Export-TwitterAuthentication
            }

        } else {
            'Failed authentication verification. Please check your credentials and try again.' | Write-Error -ErrorAction Stop
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

#endregion

#region source: \public\api\authentication\Set-TwitterBearerToken.ps1
function Set-TwitterBearerToken {
    [CmdletBinding()]
    param(
        [switch]$Persist
    )

    try {
        $Credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(('{0}:{1}' -f $OAuth['ApiKey'],$OAuth['ApiSecret'])))
        $BasicAuth = 'Basic {0}' -f $Credential

        $WebRequestParams = @{
            Uri = 'https://api.twitter.com/oauth2/token'
            Method = 'POST'
            Headers = @{ 'Authorization' = $BasicAuth }
            ContentType = 'application/x-www-form-urlencoded'
            ResponseHeadersVariable = 'TwitterResponse'
            StatusCodeVariable = 'TwitterStatusCode'
            Body = 'grant_type=client_credentials'
            Verbose = $false
        }

        $Token = Invoke-RestMethod @WebRequestParams
        $OAuth['BearerToken'] = $Token.access_token

        if ($PSBoundParameters.ContainsKey('Persist')) {
            Export-TwitterAuthentication
        }

        $ResponseData = [PsCustomObject]@{
            TwitterResponse = $TwitterResponse
            StatusCode = $TwitterStatusCode
            Uri = $WebRequestParams.Uri
            QueryString = $null
            HttpMethod = $WebRequestParams.Method
        }
        $ResponseData | Write-TwitterResponseData

    }
    catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        $_ | New-TwitterErrorRecord
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

#endregion

#region source: \public\api\authentication\Test-TwitterAuthentication.ps1
function Test-TwitterAuthentication {
    [CmdletBinding()]
    param()

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/account/verify_credentials.json',
        @{
            include_entities = 'false'
            skip_status = 'true'
        }
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters -InformationVariable Response | Out-Null
    $Response.MessageData.Status -match '200' ? $true : $false
}

#endregion

#region source: \public\api\direct_message\Get-TwitterDM.ps1
function Get-TwitterDM {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [Alias('Id')]
        [string]$DirectMessageId,
        [ValidateRange(1,50)]
        [int]$Count = 20
    )

    if ($PSBoundParameters.ContainsKey('DirectMessageId')) {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/direct_messages/events/show.json',
            @{'id' = $DirectMessageId; 'count' = $Count}
        )

        Invoke-TwitterRequest -OAuthParameters $OAuthParameters

    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/direct_messages/events/list.json',
            @{'count'= $Count }
        )

        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue events
    }
}

#endregion

#region source: \public\api\direct_message\Publish-TwitterDM.ps1
function Publish-TwitterDM {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias('id')]
        [string]$UserId,

        [Parameter(Mandatory)]
        [ValidateLength(1,10000)]
        [string]$Message,

        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('media_id')]
        [string]$MediaId
    )

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

    $OAuthParameters = [OAuthParameters]::new(
        'POST',
        'https://api.twitter.com/1.1/direct_messages/events/new.json'
    )

    if ($PSBoundParameters.ContainsKey('MediaId')) {
        $Body = $MessageWithMediaTemplate -f $UserId,$Message,$MediaId
    } else {
        $Body = $MessageTemplate -f $UserId,$Message
    }

    $OAuthParameters.Body = $Body.Replace("`r`n",'\n')

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\direct_message\Unpublish-TwitterDM.ps1
function Unpublish-TwitterDM {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('Id')]
        [string]$DirectMessageId
    )

    $OAuthParameters = [OAuthParameters]::new(
        'DELETE',
        'https://api.twitter.com/1.1/direct_messages/events/destroy.json',
        @{'id'= $DirectMessageId }
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\lists\Get-TwitterList.ps1
function Get-TwitterList {
    [CmdletBinding(DefaultParameterSetName='ListScreenName')]
    param(
        [Parameter(ParameterSetName='ListScreenName')]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='ListUserId')]
        [ValidateNotNullOrEmpty()]
        [string]$UserId,
        [Parameter(ParameterSetName='ListScreenName')]
        [Parameter(ParameterSetName='ListUserId')]
        [switch]$OwnedListFirst,

        [Parameter(Mandatory,ParameterSetName='ShowId')]
        [ValidateNotNullOrEmpty()]
        [string]$ListId,

        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [string]$Slug,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$OwnerScreenName,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [long]$OwnerId
    )

    if ($PSCmdlet.ParameterSetName -match 'List') {

        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/lists/list.json',
            $Query
        )
        Invoke-TwitterRequest -OAuthParameters $OAuthParameters

    } else {

        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'Get',
            'https://api.twitter.com/1.1/lists/show.json',
            $Query
        )
        Invoke-TwitterRequest -OAuthParameters $OAuthParameters

    }
}

#endregion

#region source: \public\api\lists\Get-TwitterListByOwner.ps1
function Get-TwitterListByOwner {
    [CmdletBinding(DefaultParameterSetName='ScreenName')]
    param(
        [Parameter(Mandatory,ParameterSetName='ScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='UserId')]
        [ValidateNotNullOrEmpty()]
        [long]$UserId,
        [ValidateRange(1,1000)]
        [Alias('Count')]
        [int]$ResultsPerPage=20
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/lists/ownerships.json',
        $Query
    )
    Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue lists
}

#endregion

#region source: \public\api\lists\Get-TwitterListMember.ps1
function Get-TwitterListMember {
    [CmdletBinding(DefaultParameterSetName='ListId')]
    param(
        [Parameter(Mandatory,ParameterSetName='ListId')]
        [string]$ListId,

        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [string]$Slug,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$OwnerScreenName,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [long]$OwnerId,

        [ValidateRange(1,5000)]
        [int]$ResultsPerPage = 20,
        [switch]$SkipStatus,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/lists/members.json',
        $Query
    )
    Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

}

#endregion

#region source: \public\api\lists\Get-TwitterListSubscriber.ps1
function Get-TwitterListSubscriber {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='ShowId')]
        [ValidateNotNullOrEmpty()]
        [string]$ListId,

        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [string]$Slug,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$OwnerScreenName,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [long]$OwnerId,

        [ValidateRange(1,5000)]
        [int]$ResultsPerPage = 20,
        [switch]$SkipStatus,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/lists/subscribers.json',
        $Query
    )
    Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

}

#endregion

#region source: \public\api\lists\Get-TwitterListSubscription.ps1
function Get-TwitterListSubscription {
    [CmdletBinding(DefaultParameterSetName='ScreenName')]
    param(
        [Parameter(Mandatory,ParameterSetName='ScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='UserId')]
        [ValidateNotNullOrEmpty()]
        [string]$UserId,

        [ValidateRange(1,1000)]
        [int]$ResultsPerPage = 20
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/lists/subscriptions.json',
        $Query
    )
    Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue lists

}

#endregion

#region source: \public\api\lists\Get-TwitterListTweets.ps1
function Get-TwitterListTweets {
    [Alias('Get-TwitterListStatus')]
    [CmdletBinding(DefaultParameterSetName='ListId')]
    param(

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

        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [string]$Slug,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerScreenName')]
        [ValidateNotNullOrEmpty()]
        [string]$OwnerScreenName,
        [Parameter(Mandatory,ParameterSetName='ShowSlugOwnerId')]
        [ValidateNotNullOrEmpty()]
        [long]$OwnerId,

        [long]$SinceId,
        [long]$MaxId,

        [ValidateRange(1,200)]
        [long]$Count = 20,
        [switch]$ExcludeEntities,
        [switch]$ExcludeRetweets
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/lists/statuses.json',
        $Query
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters | ConvertTo-Json -Depth 10 | ConvertFrom-Json

}

#endregion

#region source: \public\api\media\Send-TwitterMedia.ps1
function Send-TwitterMedia {
    [CmdletBinding()]
    [Alias('Send-Media')]
    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,

        [ValidateCount(1,100)]
        [int[]]$AddOwners
    )

    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
        $OAuthParameters = [OAuthParameters]::new('POST',$MediaUploadUrl)
        $OAuthParameters.Form = @{
            command = 'INIT'
            total_bytes = $TotalBytes
            media_category = $MediaCategory
            media_type = $MimeType
        }
        if ($PSBoundParameters.ContainsKey('AddOwners')) {
            $OAuthParameters.Form.Add(($AddOwners -join ','))
        }

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

        $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

            $OAuthParameters = [OAuthParameters]::new('POST',$MediaUploadUrl)
            $OAuthParameters.Form = @{
                command = 'APPEND'
                media_id = $MediaId
                media_data = $Media[$Index]
                segment_index = $Index
            }

            $SendMediaAppendResultParam = @{
                OAuthParameters = $OAuthParameters
                Verbose = $false
            }
            if ($Index -eq 0) {
                $SendMediaAppendResultParam.Add('SkipHistory',$true)
            }
            $SendMediaAppendResult = Invoke-TwitterRequest @SendMediaAppendResultParam

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

        # ------------------------------------------------------------------------------------------
        # FINALIZE phase
        'Beginning FINALIZE phase' | Write-Verbose
        $OAuthParameters = [OAuthParameters]::new('POST',$MediaUploadUrl)
        $OAuthParameters.Form = @{
            command = 'FINALIZE'
            media_id = $MediaId
        }
        $SendMediaFinalizeResult = Invoke-TwitterRequest -OAuthParameters $OAuthParameters -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
            $LastTwitterCommand = Get-TwitterHistory -Last 1
            if ($LastTwitterCommand.Status -match '200') {
                'Alt image text successfully added to media' | Write-Verbose
            }
        }

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

    end {

    }
}

#endregion

#region source: \public\api\searches\Add-TwitterSavedSearch.ps1
function Add-TwitterSavedSearch {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString
    )

    $OAuthParameters = [OAuthParameters]::new(
        'POST',
        'https://api.twitter.com/1.1/saved_searches/create.json',
        @{ query = [System.Uri]::EscapeDataString($SearchString) }
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters

}

#endregion

#region source: \public\api\searches\Get-TwitterSavedSearch.ps1
function  Get-TwitterSavedSearch {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [Alias('Id')]
        [long]$SearchId
    )

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

    $OAuthParameters = [OAuthParameters]::new('GET',$Url)
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters

}

#endregion

#region source: \public\api\searches\Remove-TwitterSavedSearch.ps1
function Remove-TwitterSavedSearch {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='high')]
    param(
        [Parameter(Mandatory)]
        [Alias('Id')]
        [ValidateNotNullOrEmpty()]
        [long[]]$SearchId
    )

    begin {
        $SavedSearches = Get-TwitterSavedSearch
    }

    process {
        foreach ($Id in $SearchId) {
            $ThisSearch = $SavedSearches.Where({$_.id -eq $Id})
            $ThisSearchInfo = 'Search : {0}, Created: {1}' -f $ThisSearch.query,$ThisSearch.created_at
            if ($ThisSearch) {
                if ($PSCmdlet.ShouldProcess($ThisSearchInfo, 'Removing Saved Search')) {
                    $Url = 'https://api.twitter.com/1.1/saved_searches/destroy/{0}.json' -f $Id
                    $OAuthParameters = [OAuthParameters]::new('POST',$Url)
                    Invoke-TwitterRequest -OAuthParameters $OAuthParameters | Out-Null
                }
            } else {
                'No saved search found with SearchId of {0}' -f $Id | Write-Warning
            }
        }
    }

}

#endregion

#region source: \public\api\searches\Search-Tweet.ps1
function Search-Tweet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString,
        [ValidateRange(1,100)]
        [int]$Count=15,
        [switch]$ExcludeEntities
    )

    if (-Not (Test-SearchString -SearchString $SearchString)) {
        'Search string {0} is not valid. Please try again.' -f $SearchString | Write-Error -ErrorAction Stop
    }

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/search/tweets.json',
        $Query
    )

    $SearchTweet = Invoke-TwitterRequest -OAuthParameters $OAuthParameters

    Write-Information -MessageData $SearchTweet.search_metadata
    $SearchTweet.statuses
}

#endregion

#region source: \public\api\supporting\Export-TwitterResource.ps1
function Export-TwitterResource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='Configuration')]
        [switch]$Configuration,
        [Parameter(Mandatory,ParameterSetName='Languages')]
        [switch]$Languages
    )

    if (-Not (Test-TwitterAuthentication -Verbose:$false)) {
        'Unable to connect to Twitter. Please use Set-TwitterAuthentication to set the requried API keys and secrets. Use the -Persist switch to save the values to disk.' | Write-Error -ErrorAction Stop
        return
    }

    switch ($PSCmdlet.ParameterSetName) {
        'Configuration' {
            $Resource = Get-TwitterConfiguration -Verbose:$false
            $ResourceSavePath = $ConfigurationSavePath
        }
        'Languages' {
            $Resource = Get-TwitterLanguages -Verbose:$false
            $ResourceSavePath = $LanguagesSavePath
        }
    }

    try {
        $Resource | ConvertTo-Json | Set-Content -Path $ResourceSavePath -Force
        'Saved Twitter {0} to {1}' -f $PSCmdlet.ParameterSetName,$ResourceSavePath | Write-Verbose
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

}

#endregion

#region source: \public\api\supporting\Get-TwitterAccountSettings.ps1
function Get-TwitterAccountSettings {
    [CmdletBinding()]
    param()

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/account/settings.json'
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\supporting\Get-TwitterConfiguration.ps1
function Get-TwitterConfiguration {
    [CmdletBinding()]
    param()

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/help/configuration.json'
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\supporting\Get-TwitterLanguages.ps1
function Get-TwitterLanguages {
    [CmdletBinding()]
    param()

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/help/languages.json'
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\supporting\Get-TwitterRateLimitStatus.ps1

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

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

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters | Select-Object -ExpandProperty resources
}

#endregion

#region source: \public\api\supporting\Get-TwitterUserProfileBanner.ps1
function Get-TwitterUserProfileBanner {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='ScreenName')]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='UserId')]
        [string]$UserId
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/users/profile_banner.json',
        $Query
    )

    @(Invoke-TwitterRequest -OAuthParameters $OAuthParameters).sizes
}

#endregion

#region source: \public\api\tweets\Get-Tweet.ps1
function Get-Tweet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [Alias('Id')]
        [ValidateNotNullOrEmpty()]
        [long]$TweetId
    )

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/statuses/show.json',
        @{ 'id' = $TweetId }
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters

}

#endregion

#region source: \public\api\tweets\Get-TweetLike.ps1
function Get-TweetLike {
    [CmdLetBinding(DefaultParameterSetName='ScreenName')]
    param(
        [Parameter(Mandatory,ParameterSetName='ScreenName')]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='UserId')]
        [string]$UserId,
        [ValidateRange(1,200)]
        [int]$Count=20,
        [long]$SinceId,
        [long]$MaxId,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/favorites/list.json',
        $Query
    )
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters

}

#endregion

#region source: \public\api\tweets\Publish-Tweet.ps1
function Publish-Tweet {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(Mandatory)]
        [string]$TweetText,
        [Parameter(ParameterSetName='Reply')]
        [long]$ReplyToTweet,
        [object[]]$MediaId
    )

    # 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
    # Get-TwitterConfiguration

    $Query = [hashtable]::new()
    $Query.Add('status', [System.Uri]::EscapeDataString($TweetText))

    if ($PSCmdlet.ParameterSetName -eq 'Reply') {
        $Query.Add('in_reply_to_status_id', $ReplyToTweet)

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

    if ($MediaId.Count -gt 0) {
        $Query.Add('media_ids', ($MediaId -join ','))
    }

    $OAuthParameters = [OAuthParameters]::new(
        'POST',
        'https://api.twitter.com/1.1/statuses/update.json',
        $Query
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\tweets\Set-Retweet.ps1
function Set-Retweet {
    [CmdletBinding(DefaultParameterSetName='Retweet')]
    param(
        [Parameter(Mandatory)]
        [long]$Id,
        [Parameter(ParameterSetName='Retweet')]
        [switch]$Retweet,
        [Parameter(ParameterSetName='Unretweet')]
        [switch]$Unretweet
    )

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

    $OAuthParameters = [OAuthParameters]::new('POST',$BaseUri)
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\tweets\Set-TweetLike.ps1
function Set-TweetLike {
    [CmdletBinding(DefaultParameterSetName='Like')]
    param(
        [Parameter(Mandatory)]
        [Alias('Id)')]
        [long]$TweetId,
        [Parameter(ParameterSetName='Like')]
        [switch]$Like,
        [Parameter(ParameterSetName='Unlike')]
        [switch]$Unlike,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    if ($PSCmdlet.ParameterSetName -eq 'Like') {
        $OAuthParameters = [OAuthParameters]::new(
            'POST',
            'https://api.twitter.com/1.1/favorites/create.json',
            $Query
        )
    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'POST',
            'https://api.twitter.com/1.1/favorites/destroy.json',
            $Query
        )
    }
    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterBlocks.ps1
function Get-TwitterBlocks {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(ParameterSetName='List')]
        [switch]$List,

        [Parameter(ParameterSetName='List')]
        [switch]$SkipStatus,

        [Parameter(ParameterSetName='List')]
        [switch]$ExcludeEntities
    )

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/blocks/list.json',
            $Query
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/blocks/ids.json',
            @{ cursor = -1}
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
    }
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterFollowers.ps1
function Get-TwitterFollowers {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [string]$ScreenName,
        [string]$UserId,
        [int]$ResultsPerPage,

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

        [Parameter(ParameterSetName='List')]
        [switch]$SkipStatus,

        [Parameter(ParameterSetName='List')]
        [switch]$ExcludeEntities
    )

    if ([string]::IsNullOrWhiteSpace($ScreenName) -and [string]::IsNullOrWhiteSpace($UserId)) {
        'You must supply either a ScreenName or UserId.' | Write-Warning
        return
    } elseif ($ScreenName.Length -gt 0 -and $UserId.Length -gt 0) {
        'You must supply a ScreenName or a UserId, but not both.' | Write-Warning
        return
    }

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        if ($ResultsPerPage -and $ResultsPerPage -notin 1..200) {
            'For a list of follower objects, you can only request up to 200 user objects per page.' | Write-Warning
            return
        } else {
            $ResultsPerPage = 50
        }
    } else {
        if ($ResultsPerPage -and $ResultsPerPage -notin 1..5000) {
            'For list of follower ids, you can only request up to 5000 user is per page.' | Write-Warning
            return
        } else {
            $ResultsPerPage = 5000
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/followers/list.json',
            $Query
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/followers/ids.json',
            @{ cursor = -1}
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
    }
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterFriends.ps1
function Get-TwitterFriends {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [string]$ScreenName,
        [string]$UserId,
        [int]$ResultsPerPage,

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

        [Parameter(ParameterSetName='List')]
        [switch]$SkipStatus,

        [Parameter(ParameterSetName='List')]
        [switch]$ExcludeEntities
    )

    if ([string]::IsNullOrWhiteSpace($ScreenName) -and [string]::IsNullOrWhiteSpace($UserId)) {
        'You must supply either a ScreenName or UserId' | Write-Warning
        return
    } elseif ($ScreenName.Length -gt 0 -and $UserId.Length -gt 0) {
        'You must supply a ScreenName or a UserId, but not both' | Write-Warning
        return
    }

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        if ($ResultsPerPage -and $ResultsPerPage -notin 1..200) {
            'For a list of follower objects, you can only request up to 200 user objects per page.' | Write-Warning
            return
        } else {
            $ResultsPerPage = 100
        }
    } else {
        if ($ResultsPerPage -and $ResultsPerPage -notin 1..5000) {
            'For list of follower ids, you can only request up to 5000 user is per page.' | Write-Warning
            return
        } else {
            $ResultsPerPage = 5000
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/friends/list.json',
            $Query
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/friends/ids.json',
            @{ cursor = -1}
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
    }
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterFriendship.ps1
function Get-TwitterFriendship {
    [CmdletBinding(DefaultParameterSetName='LookupScreenName')]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LookupScreenName')]
        [ValidateCount(1,100)]
        [string[]]$ScreenName,

        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LookupUserId')]
        [ValidateCount(1,100)]
        [int[]]$UserId,

        [Parameter(Mandatory,ParameterSetName='ShowScreenName')]
        [string]$SourceScreenName,

        [Parameter(Mandatory,ParameterSetName='ShowUserId')]
        [int]$SourceUserId,

        [Parameter(Mandatory,ParameterSetName='ShowScreenName')]
        [string]$TargetScreenName,

        [Parameter(Mandatory,ParameterSetName='ShowUserId')]
        [int]$TargetUserId,

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

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

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

    )

    switch -Regex ($PSCmdlet.ParameterSetName) {
        'Lookup' {
            $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/friendships/lookup.json',
                $Query
            )
            Invoke-TwitterRequest -OAuthParameters $OAuthParameters
        }
        'Show' {
            $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/friendships/show.json',
                $Query
            )
            Invoke-TwitterRequest -OAuthParameters $OAuthParameters
        }
        'Incoming' {
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/friendships/incoming.json',
                @{ cursor = -1 }
            )
            Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
        }
        'Pending' {
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/friendships/outgoing.json',
                @{ cursor = -1 }
            )
            Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
        }
        'NoRetweets' {
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/friendships/no_retweets/ids.json'
            )
            Invoke-TwitterRequest -OAuthParameters $OAuthParameters
        }
    }
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterMutedUser.ps1
function Get-TwitterMutedUser {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(ParameterSetName='List')]
        [switch]$List,

        [Parameter(ParameterSetName='List')]
        [switch]$SkipStatus,

        [Parameter(ParameterSetName='List')]
        [switch]$ExcludeEntities
    )

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/mutes/users/list.json',
            $Query
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue users

    } else {
        $OAuthParameters = [OAuthParameters]::new(
            'GET',
            'https://api.twitter.com/1.1/mutes/users/ids.json',
            @{ cursor = -1}
        )
        Invoke-TwitterCursorRequest -OAuthParameters $OAuthParameters -ReturnValue ids
    }
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterTimeline.ps1
function Get-TwitterTimeline {
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName='Mentions')]
        [switch]$MentionsTimeline,

        [Parameter(ParameterSetName='Home')]
        [switch]$HomeTimeline,

        [Parameter(ParameterSetName='User')]
        [string]$ScreenName,
        [Parameter(ParameterSetName='User')]
        [string]$UserId,
        [Parameter(ParameterSetName='User')]
        [switch]$ExcludeRetweets,

        [ValidateRange(1,200)]
        [int]$Count = 20
    )

    # since_id
    # max_id
    # trim_user
    # include_entities
    # count - hardcoded to 200

    switch ($PSCmdlet.ParameterSetName) {
        'Mentions' {
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/statuses/mentions_timeline.json',
                @{'count' = $Count}
            )
        }
        'User' {
            if ($PSCmdlet.ParameterSetName -eq 'ScreenName') {
                $Query = @{'count' = $Count; 'screen_name' = $ScreenName}
            } else {
                $Query = @{'count' = $Count; 'user_id' = $UserId}
            }

            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/statuses/user_timeline.json',
                $Query
            )
        }
        'Home' {
            $OAuthParameters = [OAuthParameters]::new(
                'GET',
                'https://api.twitter.com/1.1/statuses/home_timeline.json',
                @{'count' = $Count}
            )
        }
    }

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters | ConvertTo-Json -Depth 20 | ConvertFrom-Json
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterUser.ps1
function Get-TwitterUser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='ScreenName')]
        [string]$ScreenName,
        [Parameter(Mandatory,ParameterSetName='UserId')]
        [string]$UserId,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters
    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/users/show.json',
        $Query
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters
}

#endregion

#region source: \public\api\users_followers_friends\Get-TwitterUserList.ps1
function Get-TwitterUserList {
    [CmdletBinding()]
    param(
        [ValidateCount(1,100)]
        [string[]]$ScreenName,
        [ValidateCount(1,100)]
        [string[]]$UserId,
        [switch]$ExcludeEntities
    )

    $Query = New-TwitterQuery -ApiParameters $PSBoundParameters

    $OAuthParameters = [OAuthParameters]::new(
        'GET',
        'https://api.twitter.com/1.1/users/lookup.json',
        $Query
    )

    Invoke-TwitterRequest -OAuthParameters $OAuthParameters | ConvertTo-Json -Depth 20 | ConvertFrom-Json
}

#endregion

#region source: \public\helpers\Get-TwitterApiEndpoint.ps1
function Get-TwitterApiEndpoint {
    [CmdLetBinding(DefaultParameterSetName='Resource')]
    param(
        [Parameter(ParameterSetName='Resource')]
        [ValidateSet(
            'account','application','blocks','direct_messages','favorites','followers',
            'friends','friendships','help','lists','media','mutes','oauth2','saved_searches',
            'search','statuses','users'
        )]
        [string[]]$Resource,
        [Parameter(ParameterSetName='Command')]
        [string]$Command
    )

    $ParameterFormat = 'Name','PSParameter','Implemented','Required','Description','DefaultValue',
        'MinValue','MaxValue','Example'

    $EndpointFormat = @{l='Endpoint';e={ '{0} {1}' -f $_.Method.ToUpper(), $_.Resource}},'Function',
        'ApiVersion','Resource','Method','Uri','ApiReference','Description','Iteration',
        @{l='Parameters';e={
            foreach ($Param in $_.Parameters) {
                [PSCustomObject]$Param | Select-Object $ParameterFormat
            }
        }}

    $TwitterApiEndpoints = Get-Content -Path $ApiEndpointsPath -Raw | ConvertFrom-Json -Depth 20 -AsHashtable

    switch ($PSCmdlet.ParameterSetName) {
        'Resource' {
            if ($PSBoundParameters.ContainsKey('Resource')) {
                $Resource | ForEach-Object {
                    $TwitterApiEndpoints.$_.Values | Select-Object $EndpointFormat
                }
            } else {
                $TwitterApiEndpoints.values.values | Select-Object $EndpointFormat
            }
        }
        'Command' {
            $TwitterApiEndpoints.Values.Values.Where{$_.Function -eq $Command} | ForEach-Object {
                [PSCustomObject]$_ | Select-Object $EndpointFormat
            }
        }
    }

}

#endregion

#region source: \public\helpers\Get-TwitterHistory.ps1
function Get-TwitterHistory {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(ParameterSetName='First')]
        [ValidateRange(1,[int]::MaxValue)]
        [int]$First,
        [Parameter(ParameterSetName='Last')]
        [ValidateRange(1,[int]::MaxValue)]
        [int]$Last
    )

    switch ($PSCmdlet.ParameterSetName) {
        'First' {
            $TwitterHistoryList | Select-Object -First $First
        }
        'Last' {
            $TwitterHistoryList | Select-Object -Last $Last
        }
        default {
            $TwitterHistoryList
        }
    }

}
#endregion

#region source: \public\helpers\Get-TwitterRateLimitWarning.ps1
function Get-TwitterRateLimitWarning {
    if ($script:RateLimitWarning) {
        'RateLimitWarning is set to Enabled'
    } else {
        'RateLimitWarning is set to Disabled'
    }
}

#endregion

#region source: \public\helpers\Set-TwitterRateLimitWarning.ps1
function Set-TwitterRateLimitWarning {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='Enable')]
        [switch]$Enable,
        [Parameter(Mandatory,ParameterSetName='Disable')]
        [switch]$Disable
    )

    if ($PSCmdlet.ParameterSetName -eq 'Enable') {
        if ($RateLimitWarning) {
            'RateLimitWarning already set to Enable'
        } else {
            $script:RateLimitWarning = $true
            'RateLimitWarning set to Enable'
        }
    } else {
        if ($RateLimitWarning) {
            $script:RateLimitWarning = $false
            'RateLimitWarning set to Disable'
        } else {
            'RateLimitWarning already set to Disable'
        }
    }
}

#endregion

#region source: \public\helpers\Test-SearchString.ps1
function Test-SearchString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SearchString
    )

    if ([System.Uri]::EscapeDataString($SearchString).Length -gt 500) {
        $false
    } else {
        $true
    }
}

#endregion

#region source: \append\InitializeClient.ps1
# import disk resource
Import-TwitterResource -ErrorMapping

Import-TwitterAuthentication
Import-TwitterResource -Configuration
Import-TwitterResource -Languages

#endregion