BluebirdPS.psm1
using namespace System.Collections using namespace System.Collections.Generic using namespace Collections.ObjectModel using namespace System.Management.Automation using namespace System.Diagnostics.CodeAnalysis using namespace Microsoft.PowerShell.Commands using namespace BluebirdPS using namespace BluebirdPS.APIV2 using namespace BluebirdPS.APIV1 # -------------------------------------------------------------------------------------------------- #region set base path variables if ($IsWindows) { $DefaultSavePath = Join-Path -Path $env:USERPROFILE -ChildPath '.BluebirdPS' } else { $DefaultSavePath = Join-Path -Path $env:HOME -ChildPath '.BluebirdPS' } #endregion #region Authentication variables and setup [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $OAuth = @{ ApiKey = $null ApiSecret = $null AccessToken = $null AccessTokenSecret = $null BearerToken = $null } #endregion #region BluebirdPS configuration variable [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $BluebirdPSConfiguration = [Configuration]@{ ConfigurationPath = Join-Path -Path $DefaultSavePath -ChildPath 'Configuration.json' CredentialsPath = Join-Path -Path $DefaultSavePath -ChildPath 'twittercred.sav' } #endregion #region other variables [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $BluebirdPSHistoryList = [List[object]]::new() #endregion function Get-ErrorCategory { [CmdletBinding(DefaultParameterSetName = 'APIV1.1')] param( [Parameter(Mandatory, ParameterSetName = 'APIV1.1')] [string]$StatusCode, [Parameter(Mandatory, ParameterSetName = 'APIV1.1')] [string]$ErrorCode, [Parameter(Mandatory, ParameterSetName = 'APIV2')] [string]$ErrorType ) if ($PSCmdlet.ParameterSetName -eq 'APIV2') { switch ($ErrorType) { 'about:blank' { return 'NotSpecified' } 'https://api.twitter.com/2/problems/not-authorized-for-resource' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/not-authorized-for-field' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/invalid-request' { return 'InvalidArgument' } 'https://api.twitter.com/2/problems/client-forbidden' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/disallowed-resource' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/unsupported-authentication' { return 'AuthenticationError' } 'https://api.twitter.com/2/problems/usage-capped' { return 'QuotaExceeded' } 'https://api.twitter.com/2/problems/streaming-connection' { return 'ConnectionError' } 'https://api.twitter.com/2/problems/client-disconnected' { return 'ConnectionError' } 'https://api.twitter.com/2/problems/operational-disconnect' { return 'ResourceUnavailable' } 'https://api.twitter.com/2/problems/rule-cap' { return 'QuotaExceeded' } 'https://api.twitter.com/2/problems/invalid-rules' { return 'InvalidArgument' } 'https://api.twitter.com/2/problems/duplicate-rules' { return 'InvalidOperation' } 'https://api.twitter.com/2/problems/resource-not-found' { return 'ObjectNotFound' } } } else { switch ($StatusCode) { 400 { switch ($ErrorCode) { 324 { return 'OperationStopped' } 325 { return 'ObjectNotFound' } { $_ -in 323, 110 } { return 'InvalidOperation' } 215 { return 'AuthenticationError' } { $_ -in 3, 7, 8, 44 } { return 'InvalidArgument' } 407 { return 'ResourceUnavailable' } } } 401 { if ($ErrorCode -in 417, 135, 32, 416) { return 'InvalidOperation' } } 403 { switch ($ErrorCode) { 326 { return 'SecurityError' } { $_ -in 200, 272, 160, 203, 431 } { return 'InvalidOperation' } { $_ -in 386, 205, 226, 327 } { return 'QuotaExceeded' } { $_ -in 99, 89 } { return 'AuthenticationError' } { $_ -in 195, 92 } { return 'ConnectionError' } { $_ -in 354, 186, 38, 120, 163 } { return 'InvalidArgument' } { $_ -in 214, 220, 261, 187, 349, 385, 415, 271, 185, 36, 63, 64, 87, 179, 93, 433, 139, 150, 151, 161, 425 } { return 'PermissionDenied' } } } 404 { if ($ErrorCode -in 34, 108, 109, 422, 421, 13, 17, 144, 34, 50) { return 'InvalidOperation' } elseif ($ErrorCode -eq 25) { return 'InvalidArgument' } } 406 { return 'InvalidData' } 409 { if ($ErrorCode -eq 355) { return 'InvalidOperation' } } 410 { if ($ErrorCode -eq 68) { return 'ConnectionError' } elseif ($ErrorCode -eq 251) { return 'NotImplemented' } } 415 { return 'LimitsExceeded' } 420 { return 'QuotaExceeded' } 422 { if ($ErrorCode -eq 404) { return 'InvalidOperation' } else { return 'InvalidArgument' } } 429 { if ($ErrorCode -eq 88) { return 'QuotaExceeded' } } 500 { if ($ErrorCode -eq 131) { return 'ResourceUnavailable' } } 503 { if ($ErrorCode -eq 130) { return 'ResourceBusy' } } } } return 'NotSpecified' } function Get-ExceptionType { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ErrorCategory ) switch ($ErrorCategory) { 'AuthenticationError' { return 'AuthenticationException' } {$_ -in 'InvalidOperation','OperationStopped', 'NotImplemented' } { return 'InvalidOperationException' } {$_ -in 'InvalidArgument','InvalidData' } { return 'InvalidArgumentException' } {$_ -in 'LimitsExceeded','QuotaExceeded' } { return 'LimitsExceededException' } {$_ -in 'PermissionDenied','ResourceBusy', 'ResourceUnavailable' } { return 'ResourceViolationException' } 'ObjectNotFound' { return 'ResourceNotFoundException' } 'SecurityError' { return 'SecurityException' } 'ConnectionError' { return 'ConnectionException' } default { return 'UnspecifiedException'} } } function Get-SendMediaStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [Alias('media_id')] [string]$MediaId, [ValidateRange(1,[int]::MaxValue)] [int]$WaitSeconds ) $Request = [TwitterRequest]@{ Endpoint = 'https://upload.twitter.com/1.1/media/upload.json' Query = @{'command' = 'STATUS'; 'media_id' = $MediaId } } if ($PSBoundParameters.ContainsKey('WaitSeconds')) { $StatusCheck = 0 do { $StatusCheck++ $Activity = 'Waiting {0} seconds before refreshing upload status for media id {1}' -f $WaitSeconds, $MediaId $CurrentOperation = 'Check status #{0}' -f $StatusCheck $Status = 'Total seconds waited {0}' -f $TotalWaitSeconds Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status Start-Sleep -Seconds $WaitSeconds $TotalWaitSeconds += $WaitSeconds $SendMediaStatus = Invoke-TwitterRequest -RequestParameters $Request if ($SendMediaStatus -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaStatus) } if ($SendMediaStatus.'processing_info'.'error') { $SendMediaStatus.'processing_info'.'error' | Write-Error -ErrorAction Stop } if ($SendMediaStatus.'processing_info'.'check_after_secs') { $WaitSeconds = $SendMediaStatus.'processing_info'.'check_after_secs' -as [int] } } while ($SendMediaStatus.'processing_info'.'state' -eq 'in_progress') Write-Progress -Activity "Media upload status check completed" -Completed $SendMediaStatus } else { Invoke-TwitterRequest -RequestParameters $Request } } function Get-TwitterException { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ExceptionType, [Parameter(Mandatory)] [string]$ErrorMessage ) switch ($ExceptionType) { AuthenticationException { return [AuthenticationException]::new($ErrorMessage) } InvalidOperationException { return [InvalidOperationException]::new($ErrorMessage) } InvalidArgumentException { return [InvalidArgumentException]::new($ErrorMessage) } LimitsExceededException { return [LimitsExceededException]::new($ErrorMessage) } ResourceViolationException { return [ResourceViolationException]::new($ErrorMessage) } ResourceNotFoundException { return [ResourceNotFoundException]::new($ErrorMessage) } SecurityException { return [SecurityException]::new($ErrorMessage) } ConnectionException { return [ConnectionException]::new($ErrorMessage) } UnspecifiedException { return [UnspecifiedException]::new($ErrorMessage) } default { return [UnspecifiedException]::new($ErrorMessage) } } } function Invoke-TwitterVerifyCredentials { [CmdletBinding()] param( [switch]$BearerToken ) if ($BearerToken.IsPresent) { $Request = [TwitterRequest]@{ OAuthVersion = 'OAuth2Bearer' Endpoint = 'https://api.twitter.com/2/users/{0}' -f $BluebirdPSConfiguration.AuthUserId } } else { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/account/verify_credentials.json' Query = @{ include_entities = 'false'; skip_status = 'true' } } } $Request.SetCommandName((Get-PSCallStack).Command[1]) try { Invoke-TwitterRequest -RequestParameters $Request $BluebirdPSConfiguration.AuthValidationDate = Get-Date } catch { $BluebirdPSConfiguration.AuthValidationDate = $null $PSCmdlet.ThrowTerminatingError($_) } } function New-TwitterErrorRecord { [CmdletBinding()] param( [Parameter(Mandatory)] [ResponseData]$ResponseData ) function GetErrorData { param($ErrorList) $AllErrors = [System.Collections.Generic.List[hashtable]]::new() foreach ($AnError in $ErrorList) { $ThisError = @{} foreach ($Property in $AnError.psobject.Properties) { $ThisError.Add($Property.Name,$Property.Value) } $AllErrors.Add($ThisError) } $AllErrors } $HttpStatusCode = $ResponseData.Status.value__.ToString() $ApiResponse = $ResponseData.ApiResponse $ErrorId = 'APIv{0}-{1}' -f $ResponseData.ApiVersion,$ResponseData.Command $AllErrors = GetErrorData -ErrorList $ApiResponse.errors if ($ApiResponse.psobject.Properties.Name -notcontains 'data') { $IsTerminatingError = $true } else { $IsTerminatingError = $false } if ($ApiResponse.Type) { $ErrorMessage = $ApiResponse.Detail $ErrorCategory = Get-ErrorCategory -ErrorType $ApiResponse.Type $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage $TwitterException.Source = $ResponseData.Command $TwitterException.Data.Add('TwitterApiError',$AllErrors) $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint) $ErrorRecord.ErrorDetails = $ErrorMessage $ErrorParams = @{ ErrorRecord = $ErrorRecord CategoryActivity = $ResponseData.Command } if ($IsTerminatingError -and $TwitterErrors.Count -eq ($i + 1)) { $ErrorParams.Add('ErrorAction','Stop') } Write-Error @ErrorParams } else { $TwitterErrors = $ApiResponse.errors for ($i = 0; $i -le $TwitterErrors.Count; $i++) { #} #foreach ($TwitterError in $ApiResponse.errors) { switch ($ResponseData.ApiVersion) { 1.1 { $ErrorCategory = Get-ErrorCategory -StatusCode $HttpStatusCode -ErrorCode $TwitterErrors[$i].Code if ($Twitter.Code -eq 415) { $ErrorMessage = 'Message size exceeds limits of 10000 characters.' } else { $ErrorMessage = $TwitterErrors[$i].Message } } 2 { $ErrorCategory = Get-ErrorCategory -ErrorType $TwitterErrors[$i].Type $ErrorMessage = $TwitterErrors[$i].Detail } } $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage $TwitterException.Source = $ResponseData.Command $TwitterException.Data.Add('TwitterApiError',$AllErrors) $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint) $ErrorRecord.ErrorDetails = $ErrorMessage $ErrorParams = @{ ErrorRecord = $ErrorRecord CategoryActivity = $ResponseData.Command } if ($IsTerminatingError -and $TwitterErrors.Count -eq ($i + 1)) { $ErrorParams.Add('ErrorAction','Stop') } Write-Error @ErrorParams } } } function New-ValidationErrorRecord { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [string]$Target, [Parameter(Mandatory)] [string]$ErrorId ) [System.Management.Automation.ErrorRecord]::new( [ValidationMetadataException]::new($Message), $ErrorId, 'InvalidArgument', $Target ) } function Set-BluebirdPSAuthUser { [CmdletBinding()] param() $Request = Invoke-TwitterVerifyCredentials if ($Request.Id) { $BluebirdPSConfiguration.AuthUserId = $Request.Id if ($BluebirdPSConfiguration.RawOutput) { $BluebirdPSConfiguration.AuthUserName = $Request.screen_name } else { $BluebirdPSConfiguration.AuthUserName = $Request.UserName } 'Set AuthUserId ({0}), AuthUserName ({1})' -f $BluebirdPSConfiguration.AuthUserId,$BluebirdPSConfiguration.AuthUserName | Write-Verbose Export-BluebirdPSConfiguration } else { 'Unable to set AuthUserId and AuthUserName' | Write-Warning } } function Set-TwitterMediaAltImageText { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [Alias('media_id')] [string]$MediaId, [Parameter(Mandatory)] [ValidateLength(1,1000)] [string]$AltImageText ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://upload.twitter.com/1.1/media/metadata/create.json' } $Request.Body = '{{"media_id":"{0}","alt_text":{{"text":"{1}"}}}}' -f $MediaId,$AltImageText Invoke-TwitterRequest -RequestParameters $Request } function Write-TwitterResponse { [CmdletBinding()] param( [Parameter(Mandatory)] [ResponseData]$ResponseData ) try { if ($ResponseData.RateLimitRemaining -eq 0) { $RateLimitReached = 'Rate limit of {0} has been reached. Please wait until {1} before making another attempt for this resource.' -f $ResponseData.RateLimit,$ResponseData.RateLimitReset $RateLimitReached | Write-Error -ErrorAction Stop } if (($ResponseData.RateLimitRemaining -le $BluebirdPSConfiguration.RateLimitThreshold -and $null -ne $ResponseData.RateLimitRemaining)) { $RateLimitMessage = 'The rate limit for this resource is {0}. There are {1} remaining calls to this resource until {2}. ' -f $ResponseData.RateLimit, $ResponseData.RateLimitRemaining, $ResponseData.RateLimitReset switch ($BluebirdPSConfiguration.RateLimitAction) { 0 { $RateLimitMessage | Write-Verbose -Verbose; break} 1 { $RateLimitMessage | Write-Warning -Warning; break} 2 { $RateLimitMessage | Write-Error ; break} } } $BluebirdPSHistoryList.Add($ResponseData) Write-Information -MessageData $ResponseData if ($LastStatusCode -eq 401) { New-TwitterErrorRecord -ResponseData $ResponseData } elseif ($BluebirdPSConfiguration.RawOutput) { $ResponseData.ApiResponse } else { switch ($ResponseData.ApiVersion) { 'oauth2' { # Set-TwitterBearerToken - the only endpoint that uses oauth2 $ResponseData.ApiResponse break } '1.1' { if ($ResponseData.Command -eq 'Set-TwitterMutedUser') { # return nothing as the returned v1.1 user 'muting' property may not have been updated # an error will still be returned if an attempt to unmute a user that hasn't been muted continue } else { [Helpers]::ParseApiV1Response($ResponseData.ApiResponse) } break } '2' { if ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'following') { [ResponseInfo]::GetUpdateFriendshipStatus($ResponseData) } elseif ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'blocking') { [ResponseInfo]::GetUserBlockStatus($ResponseData) } elseif ($ResponseData.ApiResponse.data.psobject.Properties.Name -contains 'liked') { [ResponseInfo]::GetTweetLikeStatus($ResponseData) } else { [Helpers]::ParseApiV2Response($ResponseData.ApiResponse) } break } } } } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($ResponseData.ApiResponse.psobject.Properties.Name -contains 'errors') { New-TwitterErrorRecord -ResponseData $ResponseData } } function Invoke-TwitterRequest { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [TwitterRequest]$RequestParameters ) try { if ($RequestParameters.Body -and $RequestParameters.ContentType -eq 'application/json') { try { $RequestParameters.Body | ConvertFrom-Json -Depth 10 | Out-Null } catch { $PSCmdlet.ThrowTerminatingError($_) } } switch ($RequestParameters.OAuthVersion) { 'OAuth1a' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['ApiKey'],$OAuth['ApiSecret'], $OAuth['AccessToken'],$OAuth['AccessTokenSecret'] ) } 'OAuth2Bearer' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['BearerToken'] ) } 'Basic' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['ApiKey'],$OAuth['ApiSecret'] ) } } $WebRequestParams = @{ Uri = $Authentication.Uri Method = $Authentication.HttpMethod Headers = @{ 'Authorization' = $Authentication.AuthHeader} ContentType = $RequestParameters.ContentType ResponseHeadersVariable = 'ResponseHeaders' StatusCodeVariable = 'StatusCode' SkipHttpErrorCheck = $true Verbose = $false } if ($RequestParameters.Form) { $WebRequestParams.Add('Form',$RequestParameters.Form) } elseif ($RequestParameters.Body) { $WebRequestParams.Add('Body',$RequestParameters.Body) } $ApiResponse = Invoke-RestMethod @WebRequestParams $script:LastStatusCode = $StatusCode $script:LastHeaders = $ResponseHeaders $ResponseData = [ResponseData]::new($RequestParameters,$Authentication,$ResponseHeaders,$LastStatusCode,$ApiResponse) Write-TwitterResponse -ResponseData $ResponseData # recursively call this function for pagination or cursoring $script:CurrentPage = 2 $Progress = @{ Activity = 'Retrieving paged results from Twitter API' Status = 'Current page' } if ($ResponseData.ApiResponse.psobject.Properties.Name -match 'meta|next_cursor') { Write-Progress @Progress -CurrentOperation $CurrentPage $CurrentPage++ Start-Sleep -Milliseconds (Get-Random -Minimum 300 -Maximum 600) if ($ResponseData.ApiResponse.meta.next_token.length -gt 0) { # Twitter API V2 'Returned {0} objects' -f $ResponseData.ApiResponse.meta.result_count | Write-Verbose if ($RequestParameters.Query.Keys -match 'pagination_token') { $RequestParameters.Query.Remove('pagination_token') } if ($RequestParameters.Query.Keys -match 'next_token') { $RequestParameters.Query.Remove('next_token') } if ($RequestParameters.CommandName -eq 'Search-Tweet') { $RequestParameters.Query.Add('next_token',$ResponseData.ApiResponse.meta.next_token) } else { $RequestParameters.Query.Add('pagination_token',$ResponseData.ApiResponse.meta.next_token) } Invoke-TwitterRequest -RequestParameters $RequestParameters } elseif ($ResponseData.ApiResponse.next_cursor) { # Twitter API V1.1, calls to endpoints will assume starting cursor of -1 if ($RequestParameters.Query.Keys -match 'cursor') { $RequestParameters.Query.Remove('cursor') } $RequestParameters.Query.Add('cursor',$ResponseData.ApiResponse.next_cursor) Invoke-TwitterRequest -RequestParameters $RequestParameters } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Export-TwitterAuthentication { [CmdletBinding()] param() try { if (-Not (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath)) { $Action = 'new' New-Item -Path $BluebirdPSConfiguration.CredentialsPath -Force -ItemType File | Out-Null } else { $Action = 'existing' } [SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '')] $OAuth | ConvertTo-Json | ConvertTo-SecureString -AsPlainText | ConvertFrom-SecureString | Set-Content -Path $BluebirdPSConfiguration.CredentialsPath -Force 'Saved Twitter credentials to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.CredentialsPath | Write-Verbose $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime Export-BluebirdPSConfiguration } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Import-TwitterAuthentication { [CmdletBinding()] param() 'Checking for Twitter authentication.' | Write-Verbose $BluebirdPSAuthEnvPaths = 'env:BLUEBIRDPS_API_KEY', 'env:BLUEBIRDPS_API_SECRET', 'env:BLUEBIRDPS_ACCESS_TOKEN', 'env:BLUEBIRDPS_ACCESS_TOKEN_SECRET' $BluebirdPSBearerTokenEnvPath = 'env:BLUEBIRDPS_BEARER_TOKEN' if ((Test-Path -Path $BluebirdPSAuthEnvPaths) -notcontains $false) { 'Importing Twitter authentication from environment variables.' | Write-Verbose $OAuth['ApiKey'] = $env:BLUEBIRDPS_API_KEY $OAuth['ApiSecret'] = $env:BLUEBIRDPS_API_SECRET $OAuth['AccessToken'] = $env:BLUEBIRDPS_ACCESS_TOKEN $OAuth['AccessTokenSecret'] = $env:BLUEBIRDPS_ACCESS_TOKEN_SECRET if (Test-Path -Path $BluebirdPSBearerTokenEnvPath) { $OAuth['BearerToken'] = $env:BLUEBIRDPS_BEARER_TOKEN } } elseif (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { try { 'Importing Twitter authentication from credentials file.' | Write-Verbose # read the encrypted credentials file, decrypt, and convert from JSON to object $OAuthFromDisk = Get-Content -Path $BluebirdPSConfiguration.CredentialsPath | ConvertTo-SecureString -ErrorAction Stop | ConvertFrom-SecureString -AsPlainText | ConvertFrom-Json # ensure that the credentials file has the correct keys/attributes foreach ($OAuthKey in 'ApiKey','ApiSecret','AccessToken','AccessTokenSecret','BearerToken') { if ($OAuthFromDisk.psobject.Properties.Name -notcontains $OAuthKey) { Write-Error -ErrorAction Stop } } # ensure that we have values for the four required keys if ($OAuthFromDisk.psobject.Properties.Where{$_.Name -ne 'BearerToken' -and $null -ne $_.Value}.count -eq 4) { $OAuth['ApiKey'] = $OAuthFromDisk.ApiKey $OAuth['ApiSecret'] = $OAuthFromDisk.ApiSecret $OAuth['AccessToken'] = $OAuthFromDisk.AccessToken $OAuth['AccessTokenSecret'] = $OAuthFromDisk.AccessTokenSecret } if ($null -ne $OAuthFromDisk.BearerToken) { $OAuth['BearerToken'] = $OAuthFromDisk.BearerToken } } catch { 'Unable to import Twitter authentication data from credentials file.', 'Please use the Set-TwitterAuthentication command to update the required API keys and secrets.' | Write-Warning $PSCmdlet.ThrowTerminatingError($_) } } else { 'Twitter authentication data was not discovered in environment variables or on disk in credentials file.', 'Please use the Set-TwitterAuthentication command to set the required API keys and secrets.', 'The authentication values will be encrypted and saved to disk.' | Write-Warning return } try { Invoke-TwitterVerifyCredentials | Out-Null } catch { 'Twitter authentication data appears to be invalid.','Please use the Set-TwitterAuthentication command to update your stored credentials.' | Write-Warning $PSCmdlet.WriteError($_) } if ($null -eq $BluebirdPSConfiguration.AuthUserId) { Set-BluebirdPSAuthUser } if ($null -eq $OAuth['BearerToken']) { 'Bearer token not present in Twitter authentication data.','Attempting to retrieve current bearer token from Twitter.' | Write-Verbose Set-TwitterBearerToken } try { Invoke-TwitterVerifyCredentials -BearerToken | Out-Null } catch { 'Authentication data appears to have an invalid bearer token.','Please use the Set-TwitterBearerToken command to update your stored bearer token.' | Write-Warning $PSCmdlet.WriteError($_) } Export-BluebirdPSConfiguration } function Set-TwitterAuthentication { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [SecureString]$ApiKey = (Read-Host -Prompt 'API Key' -AsSecureString), [SecureString]$ApiSecret = (Read-Host -Prompt 'API Secret' -AsSecureString), [SecureString]$AccessToken = (Read-Host -Prompt 'Access Token' -AsSecureString), [SecureString]$AccessTokenSecret = (Read-Host -Prompt 'Access Token Secret' -AsSecureString) ) try { $OAuth['ApiKey'] = $ApiKey | ConvertFrom-SecureString -AsPlainText $OAuth['ApiSecret'] = $ApiSecret | ConvertFrom-SecureString -AsPlainText $OAuth['AccessToken'] = $AccessToken | ConvertFrom-SecureString -AsPlainText $OAuth['AccessTokenSecret'] = $AccessTokenSecret | ConvertFrom-SecureString -AsPlainText if (Test-TwitterAuthentication) { 'Successfully connected to Twitter.' | Write-Verbose Set-TwitterBearerToken Set-BluebirdPSAuthUser Export-TwitterAuthentication } else { 'Failed authentication verification. Please check your credentials and try again.' | Write-Error -ErrorAction Stop } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-TwitterBearerToken { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param() try { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/oauth2/token' OAuthVersion = 'Basic' Body = 'grant_type=client_credentials' ContentType = 'application/x-www-form-urlencoded' } 'Attempting to obtain an OAuth 2.0 bearer token.' | Write-Verbose $TwitterRequest = Invoke-TwitterRequest -RequestParameters $Request $OAuth['BearerToken'] = $TwitterRequest.access_token Export-TwitterAuthentication 'OAuth 2.0 bearer token successfully set.' | Write-Verbose } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Test-TwitterAuthentication { [CmdletBinding()] param( [switch]$BearerToken ) Invoke-TwitterVerifyCredentials @PSBoundParameters | Out-Null if ($LastStatusCode -eq '200') { $true $BluebirdPSConfiguration.AuthValidationDate = Get-Date } else { $false $BluebirdPSConfiguration.AuthValidationDate = $null } Export-BluebirdPSConfiguration } function Get-TwitterDM { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Id, [ValidateRange(1,50)] [int]$MessageCount = 20 ) if ($PSBoundParameters.ContainsKey('Id')) { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/show.json' Query = @{'id' = $Id } } } else { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/list.json' Query = @{'count'= $MessageCount } } } Invoke-TwitterRequest -RequestParameters $Request } function Publish-TwitterDM { [CmdletBinding(DefaultParameterSetName='DM')] param( [Parameter(Mandatory)] [ValidateLength(1,10000)] [string]$Message, [Parameter(Mandatory,ParameterSetName='DM',ValueFromPipeline)] [Parameter(Mandatory,ParameterSetName='DMWithMedia',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='DMUserObject',ValueFromPipeline)] [Parameter(Mandatory,ParameterSetName='DMWithMedia',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='DM')] [Parameter(ParameterSetName='DMUserObject')] [ValidateNotNullOrEmpty()] [string]$MediaId, [Parameter(Mandatory,ParameterSetName='DMWithMedia')] [ValidateScript({Test-Path -Path $_})] [string]$Path, [Parameter(Mandatory,ParameterSetName='DMWithMedia')] [ValidateSet('DMImage','DMVideo','DMGif')] [string]$Category, [Parameter(ParameterSetName='DMWithMedia')] [ValidateLength(1,1000)] [string]$AltImageText ) if ($PSCmdlet.ParameterSetName -eq 'DMWithMedia') { $TwitterMediaParams = @{ Path = $Path Category = $Category } if ($AltImageText) { $TwitterMediaParams.Add('AltImageText',$AltImageText) } $MediaId = Send-TwitterMedia @TwitterMediaParams | Select-Object -ExpandProperty media_id } $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 ($MediaId) { $Body = $MessageWithMediaTemplate -f $Id,$Message,$MediaId } else { $Body = $MessageTemplate -f $Id,$Message } $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)] [ValidateNotNullOrEmpty()] [BluebirdPS.APIV1.DirectMessage]$TwitterDM ) $DMId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $TwitterDM.Id $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/destroy.json' Query = @{ 'id' = $DMId} } if ($PSCmdlet.ShouldProcess($DMId, 'Removing direct message')) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null if ($LastStatusCode -eq 204) { 'Successfully deleted message with id {0} for you only. You cannot delete a message from another user`s direct messages.' -f $DMId } } } function Add-TwitterList { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^([a-zA-Z0-9]|_|-){1,25}$', ErrorMessage = "The list name '{0}' is not valid. It must be 1-25 alphanumeric characters with underlines or dashes.")] [string]$Name, [ValidateNotNullOrEmpty()] [string]$Description, [ValidateSet('Private','Public')] [string]$Mode = 'Public' ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/lists/create.json' Query = @{ 'name' = $Name 'mode' = $Mode.ToLower() 'description' = $Description } } 'Creating list {0} with mode {1}' -f $Name,$Mode | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Add-TwitterListMember { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [string[]]$UserName ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Query = @{ 'screen_name' = $UserName -join ',' } } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } if ($UserName.Count -gt 1) { $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/create_all.json' if ($UserName.Count -le 5) { $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName -join ','),$UserName.Count } else { $UserInfo = 'UserNames: {0}, Total Users: {1}' -f (($UserName[0..4] -join ',') + '...' ),$UserName.Count } } else { $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/create.json' $UserInfo = 'UserName: {0}' -f $UserName } 'Adding users to list: {0} - {1}' -f $ListInfo,$UserInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Add-TwitterListSubscription { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/create.json' } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add( 'list_id', $Id ) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add( 'list_id', $List.Id ) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } 'Subscribing to list: {0}' -f $ListInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request | Out-Null } function Get-TwitterList { [CmdletBinding(DefaultParameterSetName='ByListUserName')] param( [Parameter(ParameterSetName='ByListUserName')] [string]$UserName, [Parameter(ParameterSetName='ByListUserName')] [switch]$OwnedListFirst, [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='BySlug')] [ValidateNotNullOrEmpty()] [string]$Slug, [Parameter(ParameterSetName='BySlug')] [ValidateNotNullOrEmpty()] [string]$OwnerUserName ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/show.json' } switch ($PSCmdlet.ParameterSetName) { 'ByListUserName' { $Request.Endpoint = 'https://api.twitter.com/1.1/lists/list.json' if ($UserName -ne [String]::Empty) { $Request.Query.Add('screen_name', $UserName) } if ($OwnedListFirst.IsPresent) { $Request.Query.Add( 'reverse', $true) } } 'ById' { $Request.Query.Add( 'list_id', $Id ) } 'BySlug' { $Request.Query.Add( 'slug', $Slug) if ($PSBoundParameters.ContainsKey('OwnerUserName')) { $Request.Query.Add('owner_screen_name', $OwnerUserName) } else { $Request.Query.Add('owner_screen_name', $BluebirdPSConfiguration.AuthUserName) } } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListByOwner { [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$UserName ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/ownerships.json' Query = @{ screen_name = $PSBoundParameters.ContainsKey('UserName') ? $UserName : $BluebirdPSConfiguration.AuthUserName count = 1000 } } 'Getting lists owned by: {0}' -f $Request.Query.'screen_name' | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListMember { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/members.json' Query = @{ 'skip_status' = $true 'include_entities' = $true 'count' = 5000 } } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add( 'list_id', $Id ) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add( 'list_id', $List.Id ) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } 'Getting members of list: {0}' -f $ListInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request | Select-Object -ExpandProperty UserName } function Get-TwitterListMembership { [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$UserName, [switch]$OwnedLists ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/memberships.json' Query = @{ 'cursor' = -1 'count' = 1000 } } if ($PSBoundParameters.ContainsKey('UserName')) { $Request.Query.Add( 'screen_name', $UserName ) $UserInfo = $UserName } else { $UserInfo = $BluebirdPSConfiguration.AuthUserName } if ($OwnedLists.IsPresent) { $Request.Query.Add( 'filter_to_owned_lists', 'true' ) } 'Getting lists containing user: {0}' -f $UserInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListSubscriber { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/subscribers.json' Query = @{ 'skip_status' = $true 'include_entities' = $true 'count' = 5000 } } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } 'Getting subscribers for list: {0}' -f $ListInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request | Select-Object -ExpandProperty UserName } function Get-TwitterListSubscription { [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$UserName ) if ($PSBoundParameters.ContainsKey('UserName')) { $UserInfo = $UserName } else { $UserInfo = $BluebirdPSConfiguration.AuthUserName } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/subscriptions.json' Query = @{ count = 1000 cursor = -1 screen_name = $UserInfo } } 'Getting list subscriptions for user: {0}' -f $UserInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListTweets { [CmdletBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(Mandatory,ParameterSetName='ById')] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [string]$SinceId, [string]$MaxId, [switch]$ExcludeRetweets ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/statuses.json' Query = @{ count = 200 } } if ($PSBoundParameters.ContainsKey('SinceId')) { $Request.Query.Add('since_id',$SinceId) } if ($PSBoundParameters.ContainsKey('MaxId')) { $Request.Query.Add('max_id',$MaxId) } if ($ExcludeRetweets.IsPresent) { $Request.Query.Add('include_rts',$false) $RetweetInfo = ', including retweets,' } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } 'Getting tweets{0} for list: {1}' -f $RetweetInfo,$ListInfo | Write-Verbose Invoke-TwitterRequest -RequestParameters $Request } function Remove-TwitterList { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/lists/destroy.json' } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $List = Get-TwitterList -Id $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) } } if ($PSCmdlet.ShouldProcess($List.ToString(), 'Removing List')) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Remove-TwitterListMember { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [string[]]$UserName ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Query = @{ 'screen_name' = $UserName -join ',' } } if ($UserName.Count -gt 1) { $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/destroy_all.json' if ($UserName.Count -le 5) { $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName -join ','),$UserName.Count } else { $UserInfo = 'UserNames: {0}, Total Users: {1}' -f ($UserName[0..4] -join ','),$UserName.Count } } else { $Request.Endpoint = 'https://api.twitter.com/1.1/lists/members/destroy.json' $UserInfo = 'UserName: {0}' -f $UserName } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } $Target = '{0} - {1}' -f $ListInfo,$UserInfo if ($PSCmdlet.ShouldProcess($Target, 'Remove users from list')) { Invoke-TwitterRequest -RequestParameters $Request } } function Remove-TwitterListSubscription { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='Medium')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/destroy.json' } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } if ($PSCmdlet.ShouldProcess($ListInfo, 'Unsubscribing from list')) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Set-TwitterList { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [Parameter()] [ValidatePattern('^([a-zA-Z0-9]|_|-){1,25}$', ErrorMessage = "The list name '{0}' is not valid. It must be 1-25 alphanumeric characters with underlines or dashes.")] [string]$Name, [ValidateNotNullOrEmpty()] [string]$Description, [ValidateSet('Private','Public')] [string]$Mode ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/lists/update.json' } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) $ListInfo = 'Id: {0}' -f $Id } 'ByList' { $Request.Query.Add('list_id',$List.Id) $ListInfo = 'Id: {0}, Name: {1}' -f $List.Id,$List.Name } } $UpdatedProperties = 'Name','Description','Mode' | ForEach-Object { if ($PSBoundParameters.ContainsKey($_)) { if ($_ -eq 'Mode') { $Value = $PSBoundParameters[$_].ToString().ToLower() } else { $Value = $PSBoundParameters[$_] } $Request.Query.Add($_.ToLower(), $Value) $_ } } if ($PSCmdlet.ShouldProcess(($UpdatedProperties -join ', '), ("Updating list {0} properties") -f $ListInfo)) { Invoke-TwitterRequest -RequestParameters $Request } } function Test-TwitterListMembership { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [ValidateNotNullOrEmpty()] [string]$UserName ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/members/show.json' Query = @{ 'include_entities' = 'false'; 'skip_status' = 'true' } } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) } 'ByList' { $Request.Query.Add('list_id',$List.Id) } } if ($PSBoundParameters.ContainsKey('UserName')) { $Request.Query.Add( 'screen_name', $UserName ) } else { $Request.Query.Add( 'screen_name', $BluebirdPSConfiguration.AuthUserName ) } try { Invoke-TwitterRequest -RequestParameters $Request | Out-Null $true } catch { $false } } function Test-TwitterListSubscription { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [BluebirdPS.APIV1.List]$List, [ValidateNotNullOrEmpty()] [string]$UserName ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/lists/subscribers/show.json' Query = @{ 'include_entities' = 'false'; 'skip_status' = 'true' } } switch ($PSCmdlet.ParameterSetName) { 'ById' { $Request.Query.Add('list_id',$Id) } 'ByList' { $Request.Query.Add('list_id',$List.Id) } } if ($PSBoundParameters.ContainsKey('UserName')) { $Request.Query.Add( 'screen_name', $UserName ) } else { $Request.Query.Add( 'screen_name', $BluebirdPSConfiguration.AuthUserName ) } try { Invoke-TwitterRequest -RequestParameters $Request | Out-Null $true } catch { $false } } function Send-TwitterMedia { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateScript({Resolve-Path -Path $_})] [string]$Path, [Parameter(Mandatory)] [ValidateSet('TweetImage','TweetVideo','TweetGif','DMImage','DMVideo','DMGif')] [string]$Category, [ValidateLength(1,1000)] [string]$AltImageText ) begin { $MediaFileInfo = Get-ChildItem $Path # get mime type by extension, see https://github.com/SCRT-HQ/PSGSuite/blob/master/PSGSuite/Private/Get-MimeType.ps1 for inspiration # there's nothing currently in .Net Core that could derive the type from the content $MediaMimeTypes = @{ gif = 'image/gif' jpg = 'image/jpeg' jpeg = 'image/jpeg' png = 'image/png' webp = 'image/webp' mp4 = 'video/mp4' mov = 'video/quicktime' } $MimeType = $MediaMimeTypes[$MediaFileInfo.Extension.TrimStart('.')] # validate size of file # validate if detected mimetype matches category $SizeLimitExceededMessage = 'The size of media {0} exceeded the limit of {2} bytes. Please try again.' $CategoryMimeTypeMismatch = 'Category {0} does not match the media mimetype of {1}. Please try again.' $CategoryAltImgText = 'Category {0} does not allow the AltImageText. Please try again.' $ValidationErrorRecord = @{ Message = [String]::Empty Target = $MediaFileInfo.Name ErrorId = $null } switch -regex ($Category) { 'Image' { if ($MediaFileInfo.Length -gt 5MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,5MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -notmatch 'image') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } 'Video' { if ($MediaFileInfo.Length -gt 512MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,512MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -notmatch 'video') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } 'Gif' { if ($MediaFileInfo.Length -gt 15MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,15MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -ne 'image/gif') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } } if ($PSBoundParameters.ContainsKey('AltImageText') -and $MimeType -match 'video') { $ValidationErrorRecord.Message = $CategoryAltImgText -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryNoSupportForAltImgText' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } $MediaCategory = switch ($Category) { 'TweetImage' { 'tweet_image' } 'TweetVideo' { 'tweet_video' } 'TweetGif' { 'tweet_gif' } 'DMImage' { 'dm_image' } 'DMVideo' { 'dm_video' } 'DMGif' { 'dm_gif' } } $MediaUploadUrl = 'https://upload.twitter.com/1.1/media/upload.json' $TotalBytes = $MediaFileInfo.Length } process { 'Reading file {0}' -f $MediaFileInfo.FullName | Write-Verbose # read the image into memory $BufferSize = 900000 $Buffer = [Byte[]]::new($BufferSize) $Reader = [System.IO.File]::OpenRead($MediaFileInfo.FullName) $Media = [ArrayList]::new() do { $BytesRead = $Reader.Read($Buffer, 0 , $BufferSize) $null = $Media.Add([Convert]::ToBase64String($Buffer, 0, $BytesRead)) } while ($BytesRead -eq $BufferSize) $Reader.Dispose() # ------------------------------------------------------------------------------------------ # INIT phase 'Beginning INIT phase - media size {0}, category {1}, type {2}' -f $TotalBytes,$MediaCategory,$MimeType | Write-Verbose $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'INIT' total_bytes = $TotalBytes media_category = $MediaCategory media_type = $MimeType } } try { $SendMediaInitResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaInitResult-is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaInitResult) } } catch { $PSCmdlet.ThrowTerminatingError($_) } $MediaId = $SendMediaInitResult.'media_id' 'Upload for media id {0} successfully initiated' -f $MediaId | Write-Verbose # ------------------------------------------------------------------------------------------ # APPEND phase 'Beginning APPEND phase' | Write-Verbose $Index = 0 foreach ($Chunk in $Media) { $PercentComplete = (($Index + 1) / $Media.Count) * 100 $Activity = "Uploading media file '{0}' with id {1}" -f $MediaFileInfo.Name,$MediaId $CurrentOperation = "Media chunk #{0}" -f $Index $Status = "{0}% Complete:" -f $PercentComplete Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status -PercentComplete $PercentComplete $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'APPEND' media_id = $MediaId media_data = $Media[$Index] segment_index = $Index } } $SendMediaAppendResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaAppendResult -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaAppendResult) } $Index++ } Write-Progress -Activity 'Media upload append phase completed' -Completed # ------------------------------------------------------------------------------------------ # FINALIZE phase 'Beginning FINALIZE phase' | Write-Verbose $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'FINALIZE' media_id = $MediaId } } $SendMediaFinalizeResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaFinalizeResult -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaFinalizeResult) } # ------------------------------------------------------------------------------------------ # STATUS phase if ($SendMediaFinalizeResult.'processing_info'.'check_after_secs') { 'Beginning STATUS phase' | Write-Verbose $WaitSeconds = $SendMediaFinalizeResult.'processing_info'.'check_after_secs' -as [int] $SendMediaStatus = Get-SendMediaStatus -MediaId $MediaId -WaitSeconds $WaitSeconds -Verbose:$false $SendMediaCompletionResults = $SendMediaStatus } else { $SendMediaCompletionResults = $SendMediaFinalizeResult } # ------------------------------------------------------------------------------------------ # Add AltImageText phase if ($AltImageText.Length -gt 0) { 'Adding AltImageText to media {0}' -f $MediaId | Write-Verbose Set-TwitterMediaAltImageText -MediaId $MediaId -AltImageText $AltImageText -Verbose:$false | Out-Null if ($LastStatusCode -eq '200') { 'Alt image text successfully added to media' | Write-Verbose } } 'Media upload complete' | Write-Verbose $SendMediaCompletionResults } end { } } function Add-TwitterSavedSearch { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SearchString ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/saved_searches/create.json' Query = @{ query = $SearchString } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterSavedSearch { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Id ) if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/1.1/saved_searches/show/{0}.json' -f $Id } else { $Endpoint = 'https://api.twitter.com/1.1/saved_searches/list.json' } $Request = [TwitterRequest]@{ Endpoint = $Endpoint } Invoke-TwitterRequest -RequestParameters $Request } function Remove-TwitterSavedSearch { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='BySavedSearch',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [BluebirdPS.APIV1.SavedSearch]$SavedSearch ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $SavedSearch = Get-TwitterSavedSearch -Id $Id } $SearchInfo = 'Search: {0}, Created: {1}' -f $SavedSearch.Query,$SavedSearch.CreatedAt if ($SavedSearch) { if ($PSCmdlet.ShouldProcess($SearchInfo, 'Removing Saved Search')) { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/saved_searches/destroy/{0}.json' -f $SavedSearch.Id } Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } else { 'No saved search found with SearchId of {0}' -f $ThisSearchId | Write-Warning } } function Get-TwitterAccountSettings { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param() $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/account/settings.json' } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterPermissions { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param() try { $AccessLevel = $LastHeaders.'x-access-level' switch ($AccessLevel) { 'read-write-directmessages' { 'Read/Write/DirectMessages'} 'read-write' { 'Read/Write' } 'read' { 'ReadOnly' } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Get-TwitterRateLimitStatus { [CmdletBinding()] param( [ValidateSet( 'lists','application','mutes','live_video_stream','friendships','guide','auth','blocks','geo', 'users','teams','followers','collections','statuses','custom_profiles','webhooks','contacts', 'labs','i','tweet_prompts','moments','limiter_scalding_report_creation','fleets','help','feedback', 'business_experience','graphql&POST','friends','sandbox','drafts','direct_messages','media','traffic', 'account_activity','account','safety','favorites','device','tweets','saved_searches','oauth','search','trends','live_pipeline','graphql' )] [string[]]$Resources ) if ($Resources.Count -gt 0) { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json' Query = @{ 'resources' = ($Resources -join ',') } } } else { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json' } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterUserProfileBanner { [CmdletBinding()] param( [Parameter()] [string]$UserName ) if (-Not $PSBoundParameters.ContainsKey('UserName')) { $Query = @{ 'screen_name' = $BluebirdPSConfiguration.AuthUserName } } else { $Query = @{ 'screen_name' = $UserName } } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/users/profile_banner.json' Query = $Query } Invoke-TwitterRequest -RequestParameters $Request } function Publish-Tweet { [CmdletBinding(DefaultParameterSetName='Tweet')] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateLength(1,10000)] [string]$TweetText, [Parameter()] [string]$ReplyToTweet, [Parameter(ParameterSetName='Tweet')] [string[]]$MediaId, [Parameter(Mandatory,ParameterSetName='TweetWithMedia')] [ValidateScript({Test-Path -Path $_})] [string]$Path, [Parameter(Mandatory,ParameterSetName='TweetWithMedia')] [ValidateSet('TweetImage','TweetVideo','TweetGif')] [string]$Category, [Parameter(ParameterSetName='TweetWithMedia')] [ValidateLength(1,1000)] [string]$AltImageText ) # https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update # maximum of 4 pics, or 1 gif, or 1 video # count $TweetText characters # if the count is greater than allowed, suggest Send-TweetThread and fail if ($PSCmdlet.ParameterSetName -eq 'TweetWithMedia') { $SendMediaParams = @{ Path = $Path Category = $Category } if ($PSBoundParameters.ContainsKey('AltImageText')) { $SendMediaParams.Add('AltImageText',$AltImageText) } $MediaId = Send-TwitterMedia @SendMediaParams | Select-Object -ExpandProperty media_id } $Query = @{ status = $TweetText } if ($PSBoundParameters.ContainsKey('ReplyToTweet')) { $Query.Add('in_reply_to_status_id', $ReplyToTweet) # this will use the tweet id to get the screen_name and append it to the @mentions until @mentions have reached the limit. $Query.Add('auto_populate_reply_metadata', 'true') } if ($MediaId.Count -gt 0) { $Query.Add('media_ids', ($MediaId -join ',')) } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/statuses/update.json' Query = $Query } try { $Tweet = Invoke-TwitterRequest -RequestParameters $Request Get-Tweet -Id $Tweet.id } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-Retweet { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='Retweet')] param( [Parameter(Mandatory)] [string]$Id, [Parameter(ParameterSetName='Retweet')] [switch]$Retweet, [Parameter(ParameterSetName='Unretweet')] [switch]$Unretweet ) if ($PSCmdlet.ParameterSetName -eq 'Retweet') { $Endpoint = 'https://api.twitter.com/1.1/statuses/retweet/{0}.json' -f $Id } else { $Endpoint = 'https://api.twitter.com/1.1/statuses/unretweet/{0}.json' -f $Id } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $Endpoint } Invoke-TwitterRequest -RequestParameters $Request } function Unpublish-Tweet { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByTweet',ValueFromPipeline)] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $TweetId = $Id $TweetInfo = 'Id: {0}' -f $Id } else { $TweetId = $Tweet.Id $TweetInfo = 'Id: {0}, CreatedAt: {1}' -f $Tweet.Id,$Tweet.CreatedAt } if ($PSCmdlet.ShouldProcess($TweetInfo, 'Deleting Tweet')) { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/statuses/destroy/{0}.json' -f $TweetId } Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Get-TwitterFriendship { [CmdletBinding(DefaultParameterSetName='Lookup')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='Lookup')] [ValidateCount(1,100)] [string[]]$UserName, [Parameter(Mandatory,ParameterSetName='Show')] [string]$SourceUserName, [Parameter(Mandatory,ParameterSetName='Show')] [string]$TargetUserName, [Parameter(ParameterSetName='Incoming')] [switch]$Incoming, [Parameter(ParameterSetName='Pending')] [switch]$Pending, [Parameter(ParameterSetName='NoRetweets')] [switch]$NoRetweets ) $Query = @{} switch -Regex ($PSCmdlet.ParameterSetName) { 'Lookup' { $Endpoint = 'https://api.twitter.com/1.1/friendships/lookup.json' $Query.Add('screen_name',($UserName -join ',')) } 'Show' { $Endpoint = 'https://api.twitter.com/1.1/friendships/show.json' $Query.Add('source_screen_name',$SourceUserName) $Query.Add('target_screen_name',$TargetUserName) } 'Incoming' { $Endpoint = 'https://api.twitter.com/1.1/friendships/incoming.json' } 'Pending' { $Endpoint = 'https://api.twitter.com/1.1/friendships/outgoing.json' } 'NoRetweets' { $Endpoint = 'https://api.twitter.com/1.1/friendships/no_retweets/ids.json' } } $Request = [TwitterRequest]@{ Endpoint = $Endpoint Query = $Query } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterMutedUser { [CmdletBinding()] param() $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/mutes/users/ids.json' Query = @{ cursor = -1 } } Invoke-TwitterRequest -RequestParameters $Request } function Set-TwitterMutedUser { [CmdletBinding(DefaultParameterSetName='Mute')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='Mute')] [switch]$Mute, [Parameter(Mandatory,ParameterSetName='Unmute')] [switch]$Unmute ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Query = @{ 'screen_name' = $User.UserName } } if ($PSCmdlet.ParameterSetName -eq 'Mute') { $Request.Endpoint = 'https://api.twitter.com/1.1/mutes/users/create.json' } else { $Request.Endpoint = 'https://api.twitter.com/1.1/mutes/users/destroy.json' } Invoke-TwitterRequest -RequestParameters $Request } function Submit-TwitterUserAsSpam { [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$Block ) $Action = 'Report as Spam' if($Block.IsPresent) { $Action += ' and Block' } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/users/report_spam.json' Query = @{ screen_name = $User.UserName perform_block = $Block } } $Target = '{0}, CreatedAt: {1}, Description: {2}' -f $User.UserName,$User.CreatedAt,$User.Description if ($PSCmdlet.ShouldProcess($Target, $Action)) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Get-Tweet { [CmdletBinding(DefaultParameterSetName='Tweet')] param( [Parameter(Mandatory,Position=0,ParameterSetName='Tweet')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string[]]$Id, [Parameter(Mandatory,Position=0,ParameterSetName='Conversation')] [ValidateNotNullOrEmpty()] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The conversation tweet Id '{0}' is not valid.")] [string]$ConversationId, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics, [switch]$IncludeExpansions ) $Request = [TwitterRequest]@{ ExpansionType = 'Tweet' NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics IncludeExpansions = $IncludeExpansions } switch ($PSCmdlet.ParameterSetName) { 'Tweet' { if ($Id.Count -gt 1) { $Request.Query.Add('ids', ($Id -join ',')) $Request.Endpoint = 'https://api.twitter.com/2/tweets' } else { $Request.Endpoint = 'https://api.twitter.com/2/tweets/{0}' -f $Id } } 'Conversation' { $OriginalTweet = Get-Tweet -Id $ConversationId $OriginalTweet if ($OriginalTweet.CreatedAt -lt (Get-Date).AddDays(-7)) { 'As searching by ConversationId is based on recent search from the Standard product track, you can only retreive a conversation that started within the last 7 days.' | Write-Warning return } $Request.Query.Add('query',('conversation_id:{0}' -f $ConversationId)) $Request.Endpoint = 'https://api.twitter.com/2/tweets/search/recent' } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TweetLikes { [CmdLetBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')] [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')] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet ) switch ($PSCmdlet.ParameterSetName) { 'ById' { $TweetId = $Id } 'ByTweet' { $TweetId = $Tweet.Id } } Get-Tweet -Id $TweetId -IncludeExpansions | Where-Object { $_.psobject.TypeNames -contains 'BluebirdPS.APIV2.Objects.Poll' } } function Get-TwitterTimeline { [CmdletBinding(DefaultParameterSetName='User')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='User')] [ValidateSet('Retweets','Replies')] [string[]]$Exclude, [Parameter(Mandatory,ParameterSetName='Mentions')] [switch]$Mentions, [ValidateNotNullOrEmpty()] [datetime]$StartTime, [ValidateNotNullOrEmpty()] [datetime]$EndTime, [ValidateNotNullOrEmpty()] [string]$SinceId, [ValidateNotNullOrEmpty()] [string]$UntilId, [switch]$IncludeExpansions, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics ) $Request = [TwitterRequest]@{ Endpoint = $Endpoint ExpansionType = 'Tweet' NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics IncludeExpansions = $IncludeExpansions Query = @{ 'max_results' = 100 } } switch ($PSCmdlet.ParameterSetName) { 'User' { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/tweets' -f $User.Id if ($PSBoundParameters.ContainsKey('Exclude')){ $Request.Query.Add('exclude', ($Exclude.ToLower() -join ',') ) } } 'Mentions' { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/mentions' -f $User.Id } } if ($PSBoundParameters.ContainsKey('StartTime')) { $Request.Query.Add('start_time',[Helpers]::ConvertToV1Date($StartTime)) } if ($PSBoundParameters.ContainsKey('EndTime')) { $Request.Query.Add('end_time',[Helpers]::ConvertToV1Date($EndTime)) } if ($PSBoundParameters.ContainsKey('SinceId')) { $Request.Query.Add('since_id',$SinceId) } if ($PSBoundParameters.ContainsKey('UntilId')) { $Request.Query.Add('until_id',$UntilId) } Invoke-TwitterRequest -RequestParameters $Request } function Search-Tweet { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SearchString, [ValidateRange(10,100)] [int]$MaxResults=100, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics, [switch]$IncludeExpansions ) $Request = [TwitterRequest]@{ ExpansionType = 'Tweet' Endpoint = 'https://api.twitter.com/2/tweets/search/recent' Query = @{ 'query' = $SearchString 'max_results' = $MaxResults } NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics IncludeExpansions = $IncludeExpansions } Invoke-TwitterRequest -RequestParameters $Request } function Set-TweetLike { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='LikeById')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeById')] [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeById')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeByTweet')] [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeByTweet')] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet, [Parameter(ParameterSetName='LikeById')] [Parameter(ParameterSetName='LikeByTweet')] [switch]$Like, [Parameter(Mandatory,ParameterSetName='UnlikeById')] [Parameter(Mandatory,ParameterSetName='UnlikeByTweet')] [switch]$Unlike ) if ($PSCmdlet.ParameterSetName -match 'Id$') { $TweetId = $Id } else { $TweetId = $Tweet.Id } if ($PSCmdlet.ParameterSetName -match '^Like') { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/likes' -f $BluebirdPSConfiguration.AuthUserId Body = '{{"tweet_id": "{0}"}}' -f $TweetId } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/likes/{1}' -f $BluebirdPSConfiguration.AuthUserId,$TweetId } } Invoke-TwitterRequest -RequestParameters $Request } function Set-TweetReply { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='Hide')] param( [Parameter(Mandatory)] [string]$Id, [Parameter(ParameterSetName='Hide')] [switch]$Hide, [Parameter(ParameterSetName='Show')] [switch]$Show ) switch ($PSCmdlet.ParameterSetName) { 'Hide' { $Body = '{"hidden": true}' } 'Show' { $Body = '{"hidden": false}' } } $Request = [TwitterRequest]@{ HttpMethod = 'PUT' Endpoint = 'https://api.twitter.com/2/tweets/{0}/hidden' -f $Id Body = $Body } Invoke-TwitterRequest -RequestParameters $Request } function Add-TwitterFriend { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User ) switch ($PSCmdlet.ParameterSetName) { 'ById' { $Body = '{{"target_user_id": "{0}"}}' -f $Id } 'ByUser' { $Body = '{{"target_user_id": "{0}"}}' -f $User.Id } } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId Body = $Body } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterBlockedUser { [CmdletBinding()] param( [switch]$IncludeExpansions ) $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId ExpansionType = 'User' IncludeExpansions = $IncludeExpansions Query = @{ 'max_results' = 1000 } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterFollowers { [CmdletBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions ) switch ($PSCmdlet.ParameterSetName) { 'ById' { if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $Id } else { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $BluebirdPSConfiguration.AuthUserId } } 'ByUser' { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $User.Id } } $Request = [TwitterRequest]@{ ExpansionType = 'User' Endpoint = $Endpoint Query = @{'max_results' = 1000 } IncludeExpansions = $IncludeExpansions } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterFriends { [CmdletBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions ) switch ($PSCmdlet.ParameterSetName) { 'ById' { if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $Id } else { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId } } 'ByUser' { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $User.Id } } $Request = [TwitterRequest]@{ ExpansionType = 'User' Endpoint = $Endpoint Query = @{'max_results' = 1000 } IncludeExpansions = $IncludeExpansions } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterUser { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [ValidateCount(1, 100)] [string[]]$User, [switch]$IncludeExpansions ) begin { $UserNames = [List[string]]::new() $UserIds = [List[string]]::new() } process { foreach ($ThisUser in $User) { try { [long]::Parse($ThisUser) | Out-Null $UserIds.Add($ThisUser) } catch { $UserNames.Add($ThisUser) } } } end { if ($UserNames.Count -eq 0 -and $UserIds.Count -eq 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions Endpoint = 'https://api.twitter.com/2/users/by/username/{0}' -f $BluebirdPSConfiguration.AuthUserName } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } if ($UserNames.Count -gt 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions } if ($UserNames.Count -eq 1) { $Request.Endpoint = 'https://api.twitter.com/2/users/by/username/{0}' -f $UserNames[0] } else { $Request.Endpoint = 'https://api.twitter.com/2/users/by' $Request.Query = @{'usernames' = $UserNames -join ',' } } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } if ($UserIds.Count -gt 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions } if ($UserIds.Count -eq 1) { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}' -f $UserIds[0] } else { $Request.Endpoint = 'https://api.twitter.com/2/users' $Request.Query = @{'ids' = $UserIds -join ',' } } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } } } function Remove-TwitterFriend { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User ) $UserId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $User.Id $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/following/{1}' -f $BluebirdPSConfiguration.AuthUserId,$UserId } if ($PSCmdlet.ShouldProcess($UserId, 'Unfollow user')) { Invoke-TwitterRequest -RequestParameters $Request } } function Set-TwitterBlockedUser { [CmdletBinding(DefaultParameterSetName='Block')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='Block')] [switch]$Block, [Parameter(Mandatory,ParameterSetName='Unblock')] [switch]$Unblock ) if ($PSCmdlet.ParameterSetName -eq 'Block') { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId Body = '{{"target_user_id": "{0}"}}' -f $User.Id } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/blocking/{1}' -f $BluebirdPSConfiguration.AuthUserId,$User.Id } } Invoke-TwitterRequest -RequestParameters $Request } function ConvertFrom-EpochTime { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$UnixTime ) if ($UnixTime.Length -eq 10) { [DateTimeOffset]::FromUnixTimeSeconds([long]::Parse($UnixTime)).ToLocalTime().DateTime } else { [DateTimeOffset]::FromUnixTimeMilliseconds([long]::Parse($UnixTime)).ToLocalTime().DateTime } } function ConvertFrom-TwitterV1Date { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Date ) try { [datetime]::ParseExact( $Date, "ddd MMM dd HH:mm:ss zzz yyyy", [CultureInfo]::InvariantCulture ) } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Export-BluebirdPSConfiguration { [CmdletBinding()] param() try { if (-Not (Test-Path -Path $BluebirdPSConfiguration.ConfigurationPath)) { $Action = 'new' New-Item -Path $BluebirdPSConfiguration.ConfigurationPath -Force -ItemType File | Out-Null } else { $Action = 'existing' } if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime } $BluebirdPSConfiguration | ConvertTo-Json | Set-Content -Path $BluebirdPSConfiguration.ConfigurationPath -Force 'Saved BluebirdPS Configuration to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.ConfigurationPath | Write-Verbose } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Get-BluebirdPSConfiguration { [CmdletBinding()] param() $BluebirdPSConfiguration } function Get-BluebirdPSHistory { [CmdletBinding()] param( [ValidateRange(1,[int]::MaxValue)] [int]$First, [ValidateRange(1,[int]::MaxValue)] [int]$Last, [ValidateRange(1,[int]::MaxValue)] [int]$Skip, [ValidateRange(1,[int]::MaxValue)] [int]$SkipLast, [switch]$Errors ) $SelectObjectParams = @{} foreach ($Key in $PSBoundParameters.Keys) { if ($Key -notin [Cmdlet]::CommonParameters -and $Key -ne 'Errors') { $SelectObjectParams.Add($Key,$PSBoundParameters[$Key]) } } if ($Errors.IsPresent) { $SelectObjectParams.Add( 'Property', @( 'Command', 'Status' @{l='Errors';e= { if ($_.ApiResponse.Errors.Detail) { $_.ApiResponse.Errors.Detail } elseif ($_.ApiResponse.Errors.Message) { $_.ApiResponse.Errors.Message } }} ) ) } $BluebirdPSHistoryList | Select-Object @SelectObjectParams } function Get-TwitterApiEndpoint { [CmdletBinding()] param( [Parameter()] [string[]]$CommandName, [ValidateNotNullOrEmpty()] [string]$Endpoint ) if ($PSBoundParameters.ContainsKey('Endpoint')) { $TwitterEndpoints | Where-Object {$_.ApiEndpoint -match $Endpoint -and $_.CommandName -ne 'Get-TwitterApiEndpoint'} | Sort-Object -Property Visibility } else { $TwitterEndpoints | Where-Object {$_.ApiEndpoint.Count -gt 0 -and $_.CommandName -ne 'Get-TwitterApiEndpoint'} | Sort-Object -Property Visibility } } 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 } else { if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { $AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime } } $BluebirdPSConfiguration.AuthLastExportDate = $AuthLastExportDate continue } if ($null -ne $ConfigFromDisk.$ConfigValue) { 'Importing value {0} into {1}' -f $ConfigFromDisk.$ConfigValue,$ConfigValue | Write-Verbose $BluebirdPSConfiguration.$ConfigValue = $ConfigFromDisk.$ConfigValue } } '{0} imported.' -f $FileDescription | Write-Verbose } catch { '{0} appears to be corrupted. Please run Export-BluebirdPSConfiguration to regenerate.' -f $FileDescription | Write-Warning } } } function Set-BluebirdPSConfiguration { [CmdletBinding()] param( [BluebirdPS.RateLimitAction]$RateLimitAction, [int]$RateLimitThreshold, [bool]$RawOutput, [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 $PublicFunctions = (Get-Module -Name BluebirdPS).ExportedFunctions.Values.Name [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $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, ($Command.Name -notin $PublicFunctions ? 'Private' : 'Public'), $ApiEndpoint, $ApiDocumentation ) } #endregion |