BluebirdPS.psm1
using namespace System.Collections using namespace System.Collections.Generic using namespace Collections.ObjectModel using namespace System.Management.Automation using namespace System.Diagnostics.CodeAnalysis using namespace Microsoft.PowerShell.Commands using namespace BluebirdPS using namespace BluebirdPS.APIV1 using namespace BluebirdPS.APIV2 using namespace BluebirdPS.APIV2.TweetInfo using namespace BluebirdPS.APIV2.UserInfo using namespace BluebirdPS.Exceptions using namespace BluebirdPS.Validation # -------------------------------------------------------------------------------------------------- #region set base path variables if ($IsWindows) { $DefaultSavePath = Join-Path -Path $env:USERPROFILE -ChildPath '.BluebirdPS' } else { $DefaultSavePath = Join-Path -Path $env:HOME -ChildPath '.BluebirdPS' } #endregion #region Authentication variables and setup [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $OAuth = @{ ApiKey = $null ApiSecret = $null AccessToken = $null AccessTokenSecret = $null BearerToken = $null } #endregion #region BluebirdPS configuration variable [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $BluebirdPSConfiguration = [Configuration]@{ ConfigurationPath = Join-Path -Path $DefaultSavePath -ChildPath 'Configuration.json' CredentialsPath = Join-Path -Path $DefaultSavePath -ChildPath 'twittercred.sav' } #endregion #region other variables [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $BluebirdPSHistoryList = [List[ResponseData]]::new() $global:BluebirdPSLastResponse = [ResponseData]::new() #endregion #region Handle Module Removal $OnRemoveScript = { Remove-Variable -Name BluebirdPSLastResponse -Scope Global -Force } $ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $OnRemoveScript #endregion Handle Module Removal function Get-ErrorCategory { [CmdletBinding(DefaultParameterSetName = 'APIV1.1')] param( [Parameter(Mandatory, ParameterSetName = 'APIV1.1')] [string]$StatusCode, [Parameter(Mandatory, ParameterSetName = 'APIV1.1')] [string]$ErrorCode, [Parameter(Mandatory, ParameterSetName = 'APIV2')] [string]$ErrorType ) if ($PSCmdlet.ParameterSetName -eq 'APIV2') { switch ($ErrorType) { 'about:blank' { return 'NotSpecified' } 'https://api.twitter.com/2/problems/not-authorized-for-resource' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/not-authorized-for-field' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/invalid-request' { return 'InvalidArgument' } 'https://api.twitter.com/2/problems/client-forbidden' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/disallowed-resource' { return 'PermissionDenied' } 'https://api.twitter.com/2/problems/unsupported-authentication' { return 'AuthenticationError' } 'https://api.twitter.com/2/problems/usage-capped' { return 'QuotaExceeded' } 'https://api.twitter.com/2/problems/streaming-connection' { return 'ConnectionError' } 'https://api.twitter.com/2/problems/client-disconnected' { return 'ConnectionError' } 'https://api.twitter.com/2/problems/operational-disconnect' { return 'ResourceUnavailable' } 'https://api.twitter.com/2/problems/rule-cap' { return 'QuotaExceeded' } 'https://api.twitter.com/2/problems/invalid-rules' { return 'InvalidArgument' } 'https://api.twitter.com/2/problems/duplicate-rules' { return 'InvalidOperation' } 'https://api.twitter.com/2/problems/resource-not-found' { return 'ObjectNotFound' } } } else { switch ($StatusCode) { 400 { switch ($ErrorCode) { 324 { return 'OperationStopped' } 325 { return 'ObjectNotFound' } { $_ -in 323, 110 } { return 'InvalidOperation' } 215 { return 'AuthenticationError' } { $_ -in 3, 7, 8, 44 } { return 'InvalidArgument' } 407 { return 'ResourceUnavailable' } } } 401 { if ($ErrorCode -in 417, 135, 32, 416) { return 'InvalidOperation' } } 403 { switch ($ErrorCode) { { $_ -in 326,453 } { return 'SecurityError' } { $_ -in 200, 272, 160, 203, 431 } { return 'InvalidOperation' } { $_ -in 386, 205, 226, 327 } { return 'QuotaExceeded' } { $_ -in 99, 89 } { return 'AuthenticationError' } { $_ -in 195, 92 } { return 'ConnectionError' } { $_ -in 354, 186, 38, 120, 163 } { return 'InvalidArgument' } { $_ -in 214, 220, 261, 187, 349, 385, 415, 271, 185, 36, 63, 64, 87, 179, 93, 433, 139, 150, 151, 161, 425 } { return 'PermissionDenied' } } } 404 { if ($ErrorCode -in 34, 108, 109, 422, 421, 13, 17, 144, 34, 50) { return 'InvalidOperation' } elseif ($ErrorCode -eq 25) { return 'InvalidArgument' } } 406 { return 'InvalidData' } 409 { if ($ErrorCode -eq 355) { return 'InvalidOperation' } } 410 { if ($ErrorCode -eq 68) { return 'ConnectionError' } elseif ($ErrorCode -eq 251) { return 'NotImplemented' } } 415 { return 'LimitsExceeded' } 420 { return 'QuotaExceeded' } 422 { if ($ErrorCode -eq 404) { return 'InvalidOperation' } else { return 'InvalidArgument' } } 429 { if ($ErrorCode -eq 88) { return 'QuotaExceeded' } } 500 { if ($ErrorCode -eq 131) { return 'ResourceUnavailable' } } 503 { if ($ErrorCode -eq 130) { return 'ResourceBusy' } } } } return 'NotSpecified' } function Get-ExceptionType { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ErrorCategory ) switch ($ErrorCategory) { 'AuthenticationError' { return 'AuthenticationException' } {$_ -in 'InvalidOperation','OperationStopped', 'NotImplemented' } { return 'InvalidOperationException' } {$_ -in 'InvalidArgument','InvalidData' } { return 'InvalidArgumentException' } {$_ -in 'LimitsExceeded','QuotaExceeded' } { return 'LimitsExceededException' } {$_ -in 'PermissionDenied','ResourceBusy', 'ResourceUnavailable' } { return 'ResourceViolationException' } 'ObjectNotFound' { return 'ResourceNotFoundException' } 'SecurityError' { return 'SecurityException' } 'ConnectionError' { return 'ConnectionException' } default { return 'UnspecifiedException'} } } function Get-SendMediaStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [Alias('media_id')] [string]$MediaId, [ValidateRange(1,[int]::MaxValue)] [int]$WaitSeconds ) $Request = [TwitterRequest]@{ Endpoint = 'https://upload.twitter.com/1.1/media/upload.json' Query = @{'command' = 'STATUS'; 'media_id' = $MediaId } } if ($PSBoundParameters.ContainsKey('WaitSeconds')) { $StatusCheck = 0 do { $StatusCheck++ $Activity = 'Waiting {0} seconds before refreshing upload status for media id {1}' -f $WaitSeconds, $MediaId $CurrentOperation = 'Check status #{0}' -f $StatusCheck $Status = 'Total seconds waited {0}' -f $TotalWaitSeconds Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status Start-Sleep -Seconds $WaitSeconds $TotalWaitSeconds += $WaitSeconds $SendMediaStatus = Invoke-TwitterRequest -RequestParameters $Request if ($SendMediaStatus -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaStatus) } if ($SendMediaStatus.'processing_info'.'error') { $SendMediaStatus.'processing_info'.'error' | Write-Error -ErrorAction Stop } if ($SendMediaStatus.'processing_info'.'check_after_secs') { $WaitSeconds = $SendMediaStatus.'processing_info'.'check_after_secs' -as [int] } } while ($SendMediaStatus.'processing_info'.'state' -eq 'in_progress') Write-Progress -Activity "Media upload status check completed" -Completed $SendMediaStatus } else { Invoke-TwitterRequest -RequestParameters $Request } } function Get-TwitterException { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ExceptionType, [Parameter(Mandatory)] [string]$ErrorMessage ) switch ($ExceptionType) { AuthenticationException { return [AuthenticationException]::new($ErrorMessage) } InvalidOperationException { return [InvalidOperationException]::new($ErrorMessage) } InvalidArgumentException { return [InvalidArgumentException]::new($ErrorMessage) } LimitsExceededException { return [LimitsExceededException]::new($ErrorMessage) } ResourceViolationException { return [ResourceViolationException]::new($ErrorMessage) } ResourceNotFoundException { return [ResourceNotFoundException]::new($ErrorMessage) } SecurityException { return [SecurityException]::new($ErrorMessage) } ConnectionException { return [ConnectionException]::new($ErrorMessage) } MetricsException { return [MetricsException]::new($ErrorMessage) } UnspecifiedException { return [UnspecifiedException]::new($ErrorMessage) } default { return [UnspecifiedException]::new($ErrorMessage) } } } function Invoke-TwitterVerifyCredentials { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param( [switch]$BearerToken ) if ($BearerToken.IsPresent) { $Request = [TwitterRequest]@{ OAuthVersion = 'OAuth2Bearer' Endpoint = 'https://api.twitter.com/2/users/{0}' -f $BluebirdPSConfiguration.AuthUserId } } else { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions Endpoint = 'https://api.twitter.com/2/users/me' } } $Request.SetCommandName((Get-PSCallStack).Command[1]) try { Invoke-TwitterRequest -RequestParameters $Request $BluebirdPSConfiguration.AuthValidationDate = Get-Date } catch { $BluebirdPSConfiguration.AuthValidationDate = $null $PSCmdlet.ThrowTerminatingError($_) } } function New-TwitterErrorRecord { [CmdletBinding()] param( [Parameter(Mandatory)] [ResponseData]$ResponseData ) function GetErrorData { param($ErrorList) $AllErrors = [System.Collections.Generic.List[hashtable]]::new() foreach ($AnError in $ErrorList) { $ThisError = @{} foreach ($Property in $AnError.psobject.Properties) { $ThisError.Add($Property.Name,$Property.Value) } $AllErrors.Add($ThisError) } $AllErrors } $ApiResponse = $ResponseData.ApiResponse $ErrorId = 'APIv{0}-{1}' -f $ResponseData.ApiVersion,$ResponseData.Command $AllErrors = GetErrorData -ErrorList $ApiResponse.errors if ($ApiResponse.psobject.Properties.Name -notcontains 'data') { $IsTerminatingError = $true } else { $IsTerminatingError = $false } if ($ApiResponse.Type) { if ($ApiResponse.errors.message) { $ErrorMessage = $ApiResponse.errors.message } else { $ErrorMessage = $ApiResponse.detail } $ErrorCategory = Get-ErrorCategory -ErrorType $ApiResponse.Type $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory if ($ResponseData.Status -eq 403) { $ErrorCategory = 'SecurityError' $ExceptionType = 'SecurityException' $ErrorMessage = "Action forbidden. Please check your permissions. You may need to update your Twitter app's Access Token permissions." $IsTerminatingError = $true } $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage $TwitterException.Source = $ResponseData.Command $TwitterException.Data.Add('TwitterApiError',$AllErrors) $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint) $ErrorRecord.ErrorDetails = $ErrorMessage $ErrorParams = @{ ErrorRecord = $ErrorRecord CategoryActivity = $ResponseData.Command } if ($IsTerminatingError) { $ErrorParams.Add('ErrorAction','Stop') } Write-Error @ErrorParams } else { $TwitterErrors = $ApiResponse.errors for ($i = 0; $i -lt $TwitterErrors.Count; $i++) { switch ($ResponseData.ApiVersion) { 1.1 { $ErrorCategory = Get-ErrorCategory -StatusCode $script:LastStatusCode -ErrorCode $TwitterErrors[$i].Code if ($Twitter.Code -eq 415) { $ErrorMessage = 'Message size exceeds limits of 10000 characters.' } else { $ErrorMessage = $TwitterErrors[$i].Message } } 2 { $ErrorCategory = Get-ErrorCategory -ErrorType $TwitterErrors[$i].Type $ErrorMessage = $TwitterErrors[$i].Detail } } if ($ErrorMessage -match '_metrics') { if ($ErrorMessage -match 'field') { $MetricsRegex = "The '\w+\.\w+' field" } else { $MetricsRegex = "'\w+\.\w+'" } $ExceptionType = 'MetricsException' if ($ErrorMessage -match 'organic_metrics') { $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'OrganicMetrics' } elseif ($ErrorMessage -match 'promoted_metrics') { $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'PromotedMetrics' } if ($ErrorMessage -match 'non_public_metrics') { $ErrorMessage = $ErrorMessage -replace $MetricsRegex,'NonPublicMetrics' } } else { $ExceptionType = Get-ExceptionType -ErrorCategory $ErrorCategory } $TwitterException = Get-TwitterException -ExceptionType $ExceptionType -ErrorMessage $ErrorMessage $TwitterException.Source = $ResponseData.Command $TwitterException.Data.Add('TwitterApiError',$AllErrors) $ErrorRecord = [ErrorRecord]::new($TwitterException,$ErrorId,$ErrorCategory,$ResponseData.Endpoint) $ErrorRecord.ErrorDetails = $ErrorMessage $ErrorParams = @{ ErrorRecord = $ErrorRecord CategoryActivity = $ResponseData.Command } if ($ExceptionType -eq 'MetricsException') { # only display this exception once despite 3 errors returned for each non_public_metrics field $ErrorParams.Add('ErrorAction','Stop') } elseif ($IsTerminatingError -and $TwitterErrors.Count -eq ($i + 1)) { $ErrorParams.Add('ErrorAction','Stop') } Write-Error @ErrorParams } } } function New-ValidationErrorRecord { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [string]$Target, [Parameter(Mandatory)] [string]$ErrorId ) [System.Management.Automation.ErrorRecord]::new( [ValidationMetadataException]::new($Message), $ErrorId, 'InvalidArgument', $Target ) } function Set-BluebirdPSAuthUser { [CmdletBinding()] param() $Request = Invoke-TwitterVerifyCredentials if ($Request.Id) { $BluebirdPSConfiguration.AuthUserId = $Request.Id $BluebirdPSConfiguration.AuthUserName = switch ($BluebirdPSConfiguration.OutputType) { 'CustomClasses' { $Request.UserName } 'PSCustomObject' { $Request.screen_name } 'JSON' { ($Request | ConvertFrom-Json -Depth 10).screen_name } } 'Set AuthUserId ({0}), AuthUserName ({1})' -f $BluebirdPSConfiguration.AuthUserId,$BluebirdPSConfiguration.AuthUserName | Write-Verbose Export-BluebirdPSConfiguration } else { 'Unable to set AuthUserId and AuthUserName' | Write-Warning } } function Set-TwitterMediaAltImageText { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(Mandatory)] [Alias('media_id')] [string]$MediaId, [Parameter(Mandatory)] [ValidateLength(1,1000)] [string]$AltImageText ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://upload.twitter.com/1.1/media/metadata/create.json' } $Request.Body = '{{"media_id":"{0}","alt_text":{{"text":"{1}"}}}}' -f $MediaId,$AltImageText Invoke-TwitterRequest -RequestParameters $Request } function Write-TwitterResponse { [CmdletBinding()] param( [Parameter(Mandatory)] [ResponseData]$ResponseData ) try { $BluebirdPSHistoryList.Add($ResponseData) Write-Information -MessageData $ResponseData [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $global:BluebirdPSLastResponse = $ResponseData switch ($BluebirdPSConfiguration.OutputType) { 'PSCustomObject' { $ResponseData.ApiResponse break } 'JSON' { $ResponseData.ApiResponse | ConvertTo-Json -Depth 25 break } } if ($LastStatusCode -eq 401) { New-TwitterErrorRecord -ResponseData $ResponseData } else { switch ($ResponseData.ApiVersion) { 'oauth2' { # Set-TwitterBearerToken - the only endpoint that uses oauth2 $ResponseData.ApiResponse break } '1.1' { if ($ResponseData.Command -eq 'Set-TwitterMutedUser') { # return nothing as the returned v1.1 user 'muting' property may not have been updated # an error will still be returned if an attempt to unmute a user that hasn't been muted continue } else { [ResponseInfo]::ParseApiV1Response($ResponseData.ApiResponse) } break } '2' { if ($ResponseData.ApiResponse.data) { switch ($ResponseData.Command) { 'Add-TwitterFriend' { [ResponseInfo]::UsersFollowingCreateResponse($ResponseData); break } 'Remove-TwitterFriend' { [ResponseInfo]::UsersFollowingDeleteResponse($ResponseData); break } 'Set-TwitterBlockedUser' { [ResponseInfo]::BlockUserMutationResponse($ResponseData); break } 'Set-TwitterMutedUser' { [ResponseInfo]::MuteUserMutationResponse($ResponseData); break } 'Set-TweetLike' { [ResponseInfo]::SetTweetLikeStatus($ResponseData); break } 'Add-TwitterList' { [ResponseInfo]::ListCreateResponse($ResponseData); break } 'Set-TwitterList' { [ResponseInfo]::ListUpdateResponse($ResponseData); break } 'Remove-TwitterList' { break } 'Add-TwitterListMember' { [ResponseInfo]::ListMutateResponse($ResponseData); break } 'Remove-TwitterListMember' { [ResponseInfo]::ListMutateResponse($ResponseData); break } 'Get-TwitterListSubscription' { [ResponseInfo]::Get2UsersIdFollowedListsResponse($ResponseData); break } 'Set-TwitterPinnedList' { [ResponseInfo]::ListPinnedResponse($ResponseData); break } 'Publish-Tweet' { $ResponseData.ApiResponse.data; break } default { [ResponseInfo]::ParseApiV2Response($ResponseData.ApiResponse); break } } break } else { if ($LastStatusCode -in 403,404) { New-TwitterErrorRecord -ResponseData $ResponseData } } } } } } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($ResponseData.ApiResponse.psobject.Properties.Name -contains 'errors') { New-TwitterErrorRecord -ResponseData $ResponseData } } function Invoke-TwitterRequest { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.TwitterRequest]$RequestParameters ) if ($RequestParameters.Body -and $RequestParameters.ContentType -eq 'application/json') { try { $RequestParameters.Body | ConvertFrom-Json -Depth 10 | Out-Null } catch [Newtonsoft.Json.JsonReaderException] { $PSCmdlet.ThrowTerminatingError($_) } } switch ($RequestParameters.OAuthVersion) { 'OAuth1a' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['ApiKey'],$OAuth['ApiSecret'], $OAuth['AccessToken'],$OAuth['AccessTokenSecret'] ) } 'OAuth2Bearer' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['BearerToken'] ) } 'Basic' { $Authentication = [Authentication]::new( $RequestParameters, $OAuth['ApiKey'],$OAuth['ApiSecret'] ) } } $WebRequestParams = @{ Uri = $Authentication.Uri Method = $Authentication.HttpMethod Headers = @{ 'Authorization' = $Authentication.AuthHeader} ContentType = $RequestParameters.ContentType ResponseHeadersVariable = 'ResponseHeaders' StatusCodeVariable = 'StatusCode' SkipHttpErrorCheck = $true Verbose = $false } $VerboseProperties = 'Method','Uri','ContentType','OAuthVersion' if ($RequestParameters.Form) { $WebRequestParams.Add('Form',$RequestParameters.Form) $VerboseProperties += 'Form' } elseif ($RequestParameters.Body) { $WebRequestParams.Add('Body',$RequestParameters.Body) $VerboseProperties += 'Body' } if ($RequestParameters.InvocationInfo.BoundParameters.ContainsKey('Verbose')) { [PSCustomObject]($WebRequestParams + @{ OAuthVersion = $RequestParameters.OAuthVersion }) | Select-Object -Property $VerboseProperties | Format-List | Out-String | Write-Verbose } try { $ApiResponse = Invoke-RestMethod @WebRequestParams } catch { $PSCmdlet.ThrowTerminatingError($_) } $script:LastStatusCode = $StatusCode $script:LastHeaders = $ResponseHeaders $ResponseData = [ResponseData]::new($RequestParameters,$Authentication,$ResponseHeaders,$LastStatusCode,$ApiResponse,$BluebirdPSConfiguration.AuthUserName) $ShouldResume = $false if ($ResponseData.RateLimitRemaining -eq 0) { $WaitUntil = New-TimeSpan -End $ResponseData.RateLimitReset $RateLimitReached = 'Rate limit of {0} has been reached.' -f $ResponseData.RateLimit,$ResponseData.RateLimitReset $RateLimitStop = 'Please wait until {0} before making another attempt for this resource.' -f $ResponseData.RateLimitReset $RateLimitWait = 'Waiting until {0} before resuming attempts for this resource.' -f $ResponseData.RateLimitReset if ($BluebirdPSConfiguration.RateLimitAction -eq [RateLimitAction]::Resume) { $RateLimitReached,$RateLimitWait | Write-Warning $ResumeTimeout = $ResponseData.RateLimitReset.AddHours(1) $SleepSeconds = 60 $WriteProgress = @{ Activity = 'Waiting for RateLimitReset time' Status = 'Waiting {0} seconds...' -f $SleepSeconds } while ($ResponseData.RateLimitReset -gt [datetime]::Now -and [datetime]::Now -lt $ResumeTimeout) { $SecondsLeft = $ResponseData.RateLimitReset.Subtract([datetime]::Now).TotalSeconds if ($SecondsLeft -le $SleepSeconds) { $SleepSeconds = $SecondsLeft } $Percent = ($WaitUntil.TotalSeconds - $SecondsLeft) / $WaitUntil.TotalSeconds * 100 Write-Progress @WriteProgress -SecondsRemaining $SecondsLeft -PercentComplete $Percent Start-Sleep -Seconds $SleepSeconds } Write-Progress @WriteProgress -SecondsRemaining 0 -Completed $ShouldResume = $true } else { $RateLimitReached,$RateLimitStop | Write-Error -ErrorAction Stop } } if (($ResponseData.RateLimitRemaining -le $BluebirdPSConfiguration.RateLimitThreshold -and $null -ne $ResponseData.RateLimitRemaining)) { $RateLimitMessage = 'The rate limit for this resource is {0}. There are {1} remaining calls to this resource until {2}. ' -f $ResponseData.RateLimit, $ResponseData.RateLimitRemaining, $ResponseData.RateLimitReset switch ($BluebirdPSConfiguration.RateLimitAction) { [RateLimitAction]::Verbose { $RateLimitMessage | Write-Verbose -Verbose; break} [RateLimitAction]::Warning { $RateLimitMessage | Write-Warning -Warning; break} [RateLimitAction]::Error { $RateLimitMessage | Write-Error ; break} [RateLimitAction]::Resume { @($RateLimitMessage, 'The RateLimitAction is set to Resume. When the rate limit has been reached, the command will wait until the time above before continuing.' ) | Write-Verbose -Verbose break } } } Write-TwitterResponse -ResponseData $ResponseData if ($RequestParameters.NoPagination) { return } elseif ($ShouldResume) { $RequestParameters.Paginate($BluebirdPSLastResponse.ApiResponse.meta.next_token) Invoke-TwitterRequest -RequestParameters $RequestParameters } else { if ($ResponseData.ApiResponse.psobject.Properties.Name -match 'meta|next_cursor') { $Progress = @{ Activity = 'Retrieving paged results from Twitter API' } if ($RequestParameters.Endpoint -match '\/2\/' -and $null -ne $ResponseData.ApiResponse.meta.next_token) { # Twitter API V2 pagination if ($ResponseData.ApiResponse.meta.result_count) { 'Returned {0} objects' -f $ResponseData.ApiResponse.meta.result_count | Write-Verbose } $RequestParameters.Paginate($ResponseData.ApiResponse.meta.next_token) } elseif ($null -ne $ResponseData.ApiResponse.next_cursor -and $ResponseData.ApiResponse.next_cursor -ne 0) { # Twitter API V1.1 cursoring, calls to endpoints will assume starting cursor of -1 $RequestParameters.Paginate($ResponseData.ApiResponse.next_cursor) } else { return } Write-Progress @Progress Start-Sleep -Milliseconds (Get-Random -Minimum 300 -Maximum 600) Invoke-TwitterRequest -RequestParameters $RequestParameters } } } function Export-TwitterAuthentication { [CmdletBinding()] param() try { if (-Not (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath)) { $Action = 'new' New-Item -Path $BluebirdPSConfiguration.CredentialsPath -Force -ItemType File | Out-Null } else { $Action = 'existing' } [SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '')] $OAuth | ConvertTo-Json | ConvertTo-SecureString -AsPlainText | ConvertFrom-SecureString | Set-Content -Path $BluebirdPSConfiguration.CredentialsPath -Force 'Saved Twitter credentials to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.CredentialsPath | Write-Verbose $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime Export-BluebirdPSConfiguration } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Import-TwitterAuthentication { [CmdletBinding()] param() 'Checking for Twitter authentication.' | Write-Verbose $BluebirdPSAuthEnvPaths = 'env:BLUEBIRDPS_API_KEY', 'env:BLUEBIRDPS_API_SECRET', 'env:BLUEBIRDPS_ACCESS_TOKEN', 'env:BLUEBIRDPS_ACCESS_TOKEN_SECRET' $BluebirdPSBearerTokenEnvPath = 'env:BLUEBIRDPS_BEARER_TOKEN' if ((Test-Path -Path $BluebirdPSAuthEnvPaths) -notcontains $false) { 'Importing Twitter authentication from environment variables.' | Write-Verbose $OAuth['ApiKey'] = $env:BLUEBIRDPS_API_KEY $OAuth['ApiSecret'] = $env:BLUEBIRDPS_API_SECRET $OAuth['AccessToken'] = $env:BLUEBIRDPS_ACCESS_TOKEN $OAuth['AccessTokenSecret'] = $env:BLUEBIRDPS_ACCESS_TOKEN_SECRET if (Test-Path -Path $BluebirdPSBearerTokenEnvPath) { $OAuth['BearerToken'] = $env:BLUEBIRDPS_BEARER_TOKEN } } elseif (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { try { 'Importing Twitter authentication from credentials file.' | Write-Verbose # read the encrypted credentials file, decrypt, and convert from JSON to object $OAuthFromDisk = Get-Content -Path $BluebirdPSConfiguration.CredentialsPath | ConvertTo-SecureString -ErrorAction Stop | ConvertFrom-SecureString -AsPlainText | ConvertFrom-Json # ensure that the credentials file has the correct keys/attributes foreach ($OAuthKey in 'ApiKey','ApiSecret','AccessToken','AccessTokenSecret','BearerToken') { if ($OAuthFromDisk.psobject.Properties.Name -notcontains $OAuthKey) { Write-Error -ErrorAction Stop } } # ensure that we have values for the four required keys if ($OAuthFromDisk.psobject.Properties.Where{$_.Name -ne 'BearerToken' -and $null -ne $_.Value}.count -eq 4) { $OAuth['ApiKey'] = $OAuthFromDisk.ApiKey $OAuth['ApiSecret'] = $OAuthFromDisk.ApiSecret $OAuth['AccessToken'] = $OAuthFromDisk.AccessToken $OAuth['AccessTokenSecret'] = $OAuthFromDisk.AccessTokenSecret } if ($null -ne $OAuthFromDisk.BearerToken) { $OAuth['BearerToken'] = $OAuthFromDisk.BearerToken } } catch { 'Unable to import Twitter authentication data from credentials file.', 'Please use the Set-TwitterAuthentication command to update the required API keys and secrets.' | Write-Warning $PSCmdlet.ThrowTerminatingError($_) } } else { 'Twitter authentication data was not discovered in environment variables or on disk in credentials file.', 'Please use the Set-TwitterAuthentication command to set the required API keys and secrets.', 'The authentication values will be encrypted and saved to disk.' | Write-Warning return } try { Invoke-TwitterVerifyCredentials | Out-Null } catch { 'Twitter authentication data appears to be invalid.','Please use the Set-TwitterAuthentication command to update your stored credentials.' | Write-Warning $PSCmdlet.WriteError($_) } if ($null -eq $BluebirdPSConfiguration.AuthUserId) { Set-BluebirdPSAuthUser } if ($null -eq $OAuth['BearerToken']) { 'Bearer token not present in Twitter authentication data.','Attempting to retrieve current bearer token from Twitter.' | Write-Verbose Set-TwitterBearerToken } try { Invoke-TwitterVerifyCredentials -BearerToken | Out-Null } catch { 'Authentication data appears to have an invalid bearer token.','Please use the Set-TwitterBearerToken command to update your stored bearer token.' | Write-Warning $PSCmdlet.WriteError($_) } Export-BluebirdPSConfiguration } function Set-TwitterAuthentication { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [SecureString]$ApiKey = (Read-Host -Prompt 'API Key' -AsSecureString), [SecureString]$ApiSecret = (Read-Host -Prompt 'API Secret' -AsSecureString), [SecureString]$AccessToken = (Read-Host -Prompt 'Access Token' -AsSecureString), [SecureString]$AccessTokenSecret = (Read-Host -Prompt 'Access Token Secret' -AsSecureString) ) try { $OAuth['ApiKey'] = $ApiKey | ConvertFrom-SecureString -AsPlainText $OAuth['ApiSecret'] = $ApiSecret | ConvertFrom-SecureString -AsPlainText $OAuth['AccessToken'] = $AccessToken | ConvertFrom-SecureString -AsPlainText $OAuth['AccessTokenSecret'] = $AccessTokenSecret | ConvertFrom-SecureString -AsPlainText if (Test-TwitterAuthentication) { 'Successfully connected to Twitter.' | Write-Verbose Set-TwitterBearerToken Set-BluebirdPSAuthUser Export-TwitterAuthentication } else { 'Failed authentication verification. Please check your credentials and try again.' | Write-Error -ErrorAction Stop } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-TwitterBearerToken { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param() try { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/oauth2/token' OAuthVersion = 'Basic' Body = 'grant_type=client_credentials' ContentType = 'application/x-www-form-urlencoded' } 'Attempting to obtain an OAuth 2.0 bearer token.' | Write-Verbose $TwitterRequest = Invoke-TwitterRequest -RequestParameters $Request $OAuth['BearerToken'] = $TwitterRequest.access_token Export-TwitterAuthentication 'OAuth 2.0 bearer token successfully set.' | Write-Verbose } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Show-TwitterAuthentication { [OutputType('BluebirdPS.TwitterAuthentication')] [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] param() if ($PSCmdlet.ShouldProcess("Twitter Authentication", "Show")) { [TwitterAuthentication]::new($OAuth) } } function Test-TwitterAuthentication { [CmdletBinding()] param( [switch]$BearerToken ) Invoke-TwitterVerifyCredentials @PSBoundParameters | Out-Null if ($LastStatusCode -eq '200') { $true $BluebirdPSConfiguration.AuthValidationDate = Get-Date } else { $false $BluebirdPSConfiguration.AuthValidationDate = $null } Export-BluebirdPSConfiguration } function Get-TwitterDM { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Id, [ValidateRange(1,50)] [int]$MessageCount = 20 ) if ($PSBoundParameters.ContainsKey('Id')) { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/show.json' Query = @{'id' = $Id } } } else { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/list.json' Query = @{'count'= $MessageCount } } } Invoke-TwitterRequest -RequestParameters $Request } function Publish-TwitterDM { [CmdletBinding(DefaultParameterSetName='DMUserId')] param( [string]$Message, [Parameter(Mandatory,ParameterSetName='DMUserId',ValueFromPipeline)] [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='DMUserObject',ValueFromPipeline)] [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='DMUserId')] [Parameter(ParameterSetName='DMUserObject')] [ValidateNotNullOrEmpty()] [string]$MediaId, [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia')] [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia')] [ValidateScript({Test-Path -Path $_})] [string]$Path, [Parameter(Mandatory,ParameterSetName='DMUserIdWithMedia')] [Parameter(Mandatory,ParameterSetName='DMUserObjectWithMedia')] [ValidateSet('DMImage','DMVideo','DMGif')] [string]$Category, [Parameter(ParameterSetName='DMUserIdWithMedia')] [Parameter(ParameterSetName='DMUserObjectWithMedia')] [ValidateLength(1,1000)] [string]$AltImageText ) $MessageTemplate = '{{"event":{{"type":"message_create","message_create":{{"target":{{"recipient_id":{0}}},"message_data":{{"text":"{1}"}}}}}}}}' $MessageWithMediaTemplate = '{{"event":{{"type":"message_create","message_create":{{"target":{{"recipient_id":{0}}},"message_data":{{"text":"{1}","attachment":{{"type":"media","media":{{"id":{2}}}}}}}}}}}}}' if ($PSCmdlet.ParameterSetName -match 'WithMedia') { $TwitterMediaParams = @{ Path = $Path Category = $Category } if ($AltImageText) { $TwitterMediaParams.Add('AltImageText',$AltImageText) } $MediaId = Send-TwitterMedia @TwitterMediaParams | Select-Object -ExpandProperty media_id } $RecipientId = $PSCmdlet.ParameterSetName -match 'DMUserObject' ? $User.Id : $Id $MessageText = [string]::IsNullOrEmpty($Message) ? [string]::Empty : $Message if ($MessageText) { if ($MediaId) { $Body = $MessageWithMediaTemplate -f $RecipientId,$MessageText,$MediaId } else { $Body = $MessageTemplate -f $RecipientId,$MessageText } } else { 'You must provide a message, media, or a message and media. Please try again.' | Write-Warning return } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/new.json' Body = $Body.Replace("`r`n",'\n') } Invoke-TwitterRequest -RequestParameters $Request } function Unpublish-TwitterDM { [CmdLetBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByDM',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV1.DirectMessage]$TwitterDM ) $DMId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $TwitterDM.Id $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/1.1/direct_messages/events/destroy.json' Query = @{ 'id' = $DMId} } if ($PSCmdlet.ShouldProcess($DMId, 'Removing direct message')) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null if ($LastStatusCode -eq 204) { 'Successfully deleted message with id {0} for you only. You cannot delete a message from another user`s direct messages.' -f $DMId } } } function Send-TwitterMedia { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateScript({Resolve-Path -Path $_})] [string]$Path, [Parameter(Mandatory)] [ValidateSet('TweetImage','TweetVideo','TweetGif','DMImage','DMVideo','DMGif')] [string]$Category, [ValidateLength(1,1000)] [string]$AltImageText ) begin { $MediaFileInfo = Get-ChildItem $Path # get mime type by extension, see https://github.com/SCRT-HQ/PSGSuite/blob/master/PSGSuite/Private/Get-MimeType.ps1 for inspiration # there's nothing currently in .Net Core that could derive the type from the content $MediaMimeTypes = @{ gif = 'image/gif' jpg = 'image/jpeg' jpeg = 'image/jpeg' png = 'image/png' webp = 'image/webp' mp4 = 'video/mp4' mov = 'video/quicktime' } $MimeType = $MediaMimeTypes[$MediaFileInfo.Extension.TrimStart('.')] # validate size of file # validate if detected mimetype matches category $SizeLimitExceededMessage = 'The size of media {0} exceeded the limit of {2} bytes. Please try again.' $CategoryMimeTypeMismatch = 'Category {0} does not match the media mimetype of {1}. Please try again.' $CategoryAltImgText = 'Category {0} does not allow the AltImageText. Please try again.' $ValidationErrorRecord = @{ Message = [String]::Empty Target = $MediaFileInfo.Name ErrorId = $null } switch -regex ($Category) { 'Image' { if ($MediaFileInfo.Length -gt 5MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,5MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -notmatch 'image') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } 'Video' { if ($MediaFileInfo.Length -gt 512MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,512MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -notmatch 'video') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } 'Gif' { if ($MediaFileInfo.Length -gt 15MB) { $ValidationErrorRecord.Message = $SizeLimitExceededMessage -f $Category,$MediaFileInfo.Name,15MB $ValidationErrorRecord.ErrorId = 'SizeLimitExceeded' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } if ($MimeType -ne 'image/gif') { $ValidationErrorRecord.Message = $CategoryMimeTypeMismatch -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryMimeTypeMismatch' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } break } } if ($PSBoundParameters.ContainsKey('AltImageText') -and $MimeType -match 'video') { $ValidationErrorRecord.Message = $CategoryAltImgText -f $Category,$MimeType $ValidationErrorRecord.ErrorId = 'MediaCategoryNoSupportForAltImgText' $PSCmdlet.ThrowTerminatingError((New-ValidationErrorRecord @ValidationErrorRecord)) } $MediaCategory = switch ($Category) { 'TweetImage' { 'tweet_image' } 'TweetVideo' { 'tweet_video' } 'TweetGif' { 'tweet_gif' } 'DMImage' { 'dm_image' } 'DMVideo' { 'dm_video' } 'DMGif' { 'dm_gif' } } $MediaUploadUrl = 'https://upload.twitter.com/1.1/media/upload.json' $TotalBytes = $MediaFileInfo.Length } process { 'Reading file {0}' -f $MediaFileInfo.FullName | Write-Verbose # read the image into memory $BufferSize = 900000 $Buffer = [Byte[]]::new($BufferSize) $Reader = [System.IO.File]::OpenRead($MediaFileInfo.FullName) $Media = [ArrayList]::new() do { $BytesRead = $Reader.Read($Buffer, 0 , $BufferSize) $null = $Media.Add([Convert]::ToBase64String($Buffer, 0, $BytesRead)) } while ($BytesRead -eq $BufferSize) $Reader.Dispose() # ------------------------------------------------------------------------------------------ # INIT phase 'Beginning INIT phase - media size {0}, category {1}, type {2}' -f $TotalBytes,$MediaCategory,$MimeType | Write-Verbose $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'INIT' total_bytes = $TotalBytes media_category = $MediaCategory media_type = $MimeType } } try { $SendMediaInitResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaInitResult-is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaInitResult) } } catch { $PSCmdlet.ThrowTerminatingError($_) } $MediaId = $SendMediaInitResult.'media_id' 'Upload for media id {0} successfully initiated' -f $MediaId | Write-Verbose # ------------------------------------------------------------------------------------------ # APPEND phase 'Beginning APPEND phase' | Write-Verbose $Index = 0 foreach ($Chunk in $Media) { $PercentComplete = (($Index + 1) / $Media.Count) * 100 $Activity = "Uploading media file '{0}' with id {1}" -f $MediaFileInfo.Name,$MediaId $CurrentOperation = "Media chunk #{0}" -f $Index $Status = "{0}% Complete:" -f $PercentComplete Write-Progress -Activity $Activity -CurrentOperation $CurrentOperation -Status $Status -PercentComplete $PercentComplete $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'APPEND' media_id = $MediaId media_data = $Media[$Index] segment_index = $Index } } $SendMediaAppendResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaAppendResult -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaAppendResult) } $Index++ } Write-Progress -Activity 'Media upload append phase completed' -Completed # ------------------------------------------------------------------------------------------ # FINALIZE phase 'Beginning FINALIZE phase' | Write-Verbose $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = $MediaUploadUrl Form = @{ command = 'FINALIZE' media_id = $MediaId } } $SendMediaFinalizeResult = Invoke-TwitterRequest -RequestParameters $Request -Verbose:$false if ($SendMediaFinalizeResult -is [ErrorRecord]) { $PSCmdlet.ThrowTerminatingError($SendMediaFinalizeResult) } # ------------------------------------------------------------------------------------------ # STATUS phase if ($SendMediaFinalizeResult.'processing_info'.'check_after_secs') { 'Beginning STATUS phase' | Write-Verbose $WaitSeconds = $SendMediaFinalizeResult.'processing_info'.'check_after_secs' -as [int] $SendMediaStatus = Get-SendMediaStatus -MediaId $MediaId -WaitSeconds $WaitSeconds -Verbose:$false $SendMediaCompletionResults = $SendMediaStatus } else { $SendMediaCompletionResults = $SendMediaFinalizeResult } # ------------------------------------------------------------------------------------------ # Add AltImageText phase if ($AltImageText.Length -gt 0) { 'Adding AltImageText to media {0}' -f $MediaId | Write-Verbose Set-TwitterMediaAltImageText -MediaId $MediaId -AltImageText $AltImageText -Verbose:$false | Out-Null if ($LastStatusCode -eq '200') { 'Alt image text successfully added to media' | Write-Verbose } } 'Media upload complete' | Write-Verbose $SendMediaCompletionResults } end { } } function Add-TwitterSavedSearch { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SearchString ) $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/saved_searches/create.json' Query = @{ query = $SearchString } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterSavedSearch { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Id ) if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/1.1/saved_searches/show/{0}.json' -f $Id } else { $Endpoint = 'https://api.twitter.com/1.1/saved_searches/list.json' } $Request = [TwitterRequest]@{ Endpoint = $Endpoint } Invoke-TwitterRequest -RequestParameters $Request } function Remove-TwitterSavedSearch { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter(Mandatory,ParameterSetName='BySavedSearch',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV1.SavedSearch]$SavedSearch ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $SavedSearch = Get-TwitterSavedSearch -Id $Id } $SearchInfo = 'Search: {0}, Created: {1}' -f $SavedSearch.Query,$SavedSearch.CreatedAt if ($SavedSearch) { if ($PSCmdlet.ShouldProcess($SearchInfo, 'Removing Saved Search')) { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/saved_searches/destroy/{0}.json' -f $SavedSearch.Id } Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } else { 'No saved search found with SearchId of {0}' -f $ThisSearchId | Write-Warning } } function Get-TwitterAccountSettings { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param() $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/account/settings.json' } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterPermissions { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param() try { $AccessLevel = $LastHeaders.'x-access-level' switch ($AccessLevel) { 'read-write-directmessages' { 'Read/Write/DirectMessages'} 'read-write' { 'Read/Write' } 'read' { 'ReadOnly' } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Get-TwitterRateLimitStatus { [CmdletBinding()] param( [ValidateSet( 'lists','application','mutes','live_video_stream','friendships','guide','auth','blocks','geo', 'users','teams','followers','collections','statuses','custom_profiles','webhooks','contacts', 'labs','i','tweet_prompts','moments','limiter_scalding_report_creation','fleets','help','feedback', 'business_experience','graphql&POST','friends','sandbox','drafts','direct_messages','media','traffic', 'account_activity','account','safety','favorites','device','tweets','saved_searches','oauth','search','trends','live_pipeline','graphql' )] [string[]]$Resources ) if ($Resources.Count -gt 0) { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json' Query = @{ 'resources' = ($Resources -join ',') } } } else { $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/application/rate_limit_status.json' } } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterUserProfileBanner { [CmdletBinding()] param( [Parameter()] [string]$UserName ) if (-Not $PSBoundParameters.ContainsKey('UserName')) { $Query = @{ 'screen_name' = $BluebirdPSConfiguration.AuthUserName } } else { $Query = @{ 'screen_name' = $UserName } } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/1.1/users/profile_banner.json' Query = $Query } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterFriendship { [CmdletBinding(DefaultParameterSetName='Lookup')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='Lookup')] [ValidateCount(1,100)] [string[]]$UserName, [Parameter(Mandatory,ParameterSetName='Show')] [string]$SourceUserName, [Parameter(Mandatory,ParameterSetName='Show')] [string]$TargetUserName, [Parameter(ParameterSetName='Incoming')] [switch]$Incoming, [Parameter(ParameterSetName='Pending')] [switch]$Pending, [Parameter(ParameterSetName='NoRetweets')] [switch]$NoRetweets ) $Query = @{} switch -Regex ($PSCmdlet.ParameterSetName) { 'Lookup' { $Endpoint = 'https://api.twitter.com/1.1/friendships/lookup.json' $Query.Add('screen_name',($UserName -join ',')) } 'Show' { $Endpoint = 'https://api.twitter.com/1.1/friendships/show.json' $Query.Add('source_screen_name',$SourceUserName) $Query.Add('target_screen_name',$TargetUserName) } 'Incoming' { $Endpoint = 'https://api.twitter.com/1.1/friendships/incoming.json' } 'Pending' { $Endpoint = 'https://api.twitter.com/1.1/friendships/outgoing.json' } 'NoRetweets' { $Endpoint = 'https://api.twitter.com/1.1/friendships/no_retweets/ids.json' } } $Request = [TwitterRequest]@{ Endpoint = $Endpoint Query = $Query } Invoke-TwitterRequest -RequestParameters $Request } function Submit-TwitterUserAsSpam { [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$Block ) $Action = 'Report as Spam' if($Block.IsPresent) { $Action += ' and Block' } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/1.1/users/report_spam.json' Query = @{ screen_name = $User.UserName perform_block = $Block } } $Target = '{0}, CreatedAt: {1}, Description: {2}' -f $User.UserName,$User.CreatedAt,$User.Description if ($PSCmdlet.ShouldProcess($Target, $Action)) { Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Add-TwitterList { [OutputType('BluebirdPS.APIV2.ListInfo.List')] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateLength(1,25)] [string]$Name, [ValidateLength(0,100)] [string]$Description, [switch]$Private ) $Body = @{ name = $Name private = $Private.IsPresent } if ($PSBoundParameters.ContainsKey('Description')) { $Body.Add('description',$Description) } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/lists' Body = [PSCustomObject]$Body | ConvertTo-Json } $AddTwitterList = Invoke-TwitterRequest -RequestParameters $Request if ($AddTwitterList) { Get-TwitterList -Id $AddTwitterList } } function Add-TwitterListMember { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [ValidateCount(1,100)] [BluebirdPS.APIV2.UserInfo.User[]]$User ) begin { if ($PSCmdlet.ParameterSetName -eq 'ById') { $ListId = $Id $List = Get-TwitterList -Id $Id } else { $ListId = $List.Id } if ($List.OwnerId -ne $BluebirdPSConfiguration.AuthUserId) { 'You must be the owner of a list to add members.' | Write-Error -ErrorAction Stop } } process { foreach ($NewMember in $User) { if (Test-TwitterListMembership -List $List -User $NewMember) { 'User {0} is already a member of list {1}' -f $NewMember.Name,$List.ToShortString() } else { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint ='https://api.twitter.com/2/lists/{0}/members' -f $ListId Body = [PSCustomObject]@{'user_id' = $NewMember.Id } | ConvertTo-Json } $Request.SetCommandName('Add-TwitterListMember') try{ $AddTwitterListMember = Invoke-TwitterRequest -RequestParameters $Request if ($AddTwitterListMember) { 'User {0} added to list {1}' -f $NewMember.Name,$List.ToShortString() } else { 'User {0} was not added to list {1}' -f $NewMember.Name,$List.ToShortString() } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } } } function Add-TwitterListSubscription { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $ListId = $Id $List = Get-TwitterList -Id $Id } else { $ListId = $List.Id } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint ='https://api.twitter.com/2/users/{0}/followed_lists' -f $BluebirdPSConfiguration.AuthUserId Body = [PSCustomObject]@{'list_id' = $ListId } | ConvertTo-Json } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterList { [OutputType( 'BluebirdPS.APIV2.ListInfo.List', 'BluebirdPS.APIV2.UserInfo.User' )] [CmdletBinding(DefaultParameterSetName='ByUser')] param( [Parameter(Mandatory,ParameterSetName='ByListId')] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(ParameterSetName='ByUser',ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions, [ValidateNotNullOrEmpty()] [string]$SearchName ) if ($PSCmdlet.ParameterSetName -eq 'ByListId') { $Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $Id } else { $UserId = $User.Id ? $User.Id : $BluebirdPSConfiguration.AuthUserId $Endpoint = 'https://api.twitter.com/2/users/{0}/owned_lists' -f $UserId } $Request = [TwitterRequest]@{ ExpansionType = 'List' Endpoint = $Endpoint IncludeExpansions = $IncludeExpansions } if ($PSBoundParameters.ContainsKey('SearchName')) { Invoke-TwitterRequest -RequestParameters $Request | Where-Object Name -match $SearchName } else { Invoke-TwitterRequest -RequestParameters $Request } } function Get-TwitterListMember { [OutputType( 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.TweetInfo.Tweet' )] [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [switch]$IncludeExpansions, [ValidateRange(1,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) $ListId = $PSCmdlet.ParameterSetName -eq 'ById' ? $Id : $List.Id if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint ='https://api.twitter.com/2/lists/{0}/members' -f $ListId Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListMembership { [OutputType( 'BluebirdPS.APIV2.ListInfo.List', 'BluebirdPS.APIV2.UserInfo.User' )] [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions, [ValidateRange(1,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) if ($null -eq $User.Id) { $UserId = $BluebirdPSConfiguration.AuthUserId } else { $UserId = $User.Id } if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint ='https://api.twitter.com/2/users/{0}/list_memberships' -f $UserId Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'List' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListSubscriber { [OutputType( 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.TweetInfo.Tweet' )] [CmdLetBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [switch]$IncludeExpansions, [ValidateRange(1,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/lists/{0}/followers' -f $List.Id Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListSubscription { [OutputType('BluebirdPS.APIV2.ListInfo.List')] [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions, [ValidateRange(1,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) $UserId = $User.Id ? $User.Id : $BluebirdPSConfiguration.AuthUserId if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint ='https://api.twitter.com/2/users/{0}/followed_lists' -f $UserId Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'List' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterListTweets { [OutputType( 'BluebirdPS.APIV2.TweetInfo.Tweet', 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.MediaInfo.Media', 'BluebirdPS.APIV2.Objects.Poll' )] [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [switch]$IncludeExpansions, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics, [ValidateRange(10,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/lists/{0}/tweets' -f $List.Id Query = @{ 'max_results' = $MaxResultsPerPage } ExpansionType = 'Tweet' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterPinnedList { [OutputType('BluebirdPS.APIV2.ListInfo.List')] [CmdletBinding()] param() # this endpoint can return pinned lists for any user; this command needs to be updated. $Request = [TwitterRequest]@{ Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists' -f $BluebirdPSConfiguration.AuthUserId ExpansionType = 'List' } Invoke-TwitterRequest -RequestParameters $Request } function Remove-TwitterList { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List ) switch ($PSCmdlet.ParameterSetName) { 'ById' { $ListId = $Id $List = Get-TwitterList -Id $Id } 'ByList' { $ListId = $List.Id } } $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $ListId } if ($PSCmdlet.ShouldProcess($List.ToString(), 'Removing List')) { Invoke-TwitterRequest -RequestParameters $Request } } function Remove-TwitterListMember { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [Parameter(ValueFromPipeline)] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [BluebirdPS.APIV2.UserInfo.User[]]$User ) begin { if ($PSCmdlet.ParameterSetName -eq 'ById') { $ListId = $Id $List = Get-TwitterList -Id $Id } else { $ListId = $List.Id } if ($List.OwnerId -ne $BluebirdPSConfiguration.AuthUserId) { 'You must be the owner of a list to remove members.' | Write-Error -ErrorAction Stop } } process { foreach ($RemoveMember in $User) { if (Test-TwitterListMembership -List $List -User $RemoveMember) { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint ='https://api.twitter.com/2/lists/{0}/members/{1}' -f $ListId,$RemoveMember.Id } $Request.SetCommandName('Remove-TwitterListMember') try { $RemoveTwitterListMember = Invoke-TwitterRequest -RequestParameters $Request if ($RemoveTwitterListMember) { 'User {0} removed from list {1}' -f $RemoveMember.Name,$List.ToShortString() } else { 'User {0} was not removed from list {1}' -f $RemoveMember.Name,$List.ToShortString() } } catch { $PSCmdlet.ThrowTerminatingError($_) } } else { 'User {0} is not a member of list {1}' -f $RemoveMember.Name,$List.ToShortString() } } } } function Remove-TwitterListSubscription { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(ParameterSetName='ById',Mandatory,ValueFromPipeline)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(ParameterSetName='ByList',Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $ListId = $Id $List = Get-TwitterList -Id $Id } else { $ListId = $List.Id } $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint ='https://api.twitter.com/2/users/{0}/followed_lists/{1}' -f $BluebirdPSConfiguration.AuthUserId,$ListId } Invoke-TwitterRequest -RequestParameters $Request } function Set-TwitterList { [OutputType('BluebirdPS.APIV2.ListInfo.List')] [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById')] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The List Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByList',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [Parameter()] [ValidateLength(1,25)] [string]$Name, [ValidateLength(0,100)] [string]$Description, [boolean]$Private ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $ListId = $Id $List = Get-TwitterList -Id $Id } else { $ListId = $List.Id } $Body = @{} 'Name','Description','Private' | ForEach-Object { if ($PSBoundParameters.ContainsKey($_)) { $Body.Add($_.ToLower(), $PSBoundParameters[$_]) } } $Request = [TwitterRequest]@{ HttpMethod = 'PUT' Endpoint = 'https://api.twitter.com/2/lists/{0}' -f $ListId Body = [PSCustomObject]$Body | ConvertTo-Json } try{ if ($PSCmdlet.ShouldProcess($List.ToString(),'Update list')) { $SetTwitterList = Invoke-TwitterRequest -RequestParameters $Request if ($SetTwitterList) { Get-TwitterList -Id $SetTwitterList } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-TwitterPinnedList { [CmdletBinding(DefaultParameterSetName='PinList')] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [Parameter(ParameterSetName='PinList')] [switch]$PinList, [Parameter(ParameterSetName='UnpinList')] [switch]$UnpinList ) if ($PSCmdlet.ParameterSetName -eq 'PinList') { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists' -f $BluebirdPSConfiguration.AuthUserId Body = [PSCustomObject]@{ 'list_id' = $List.Id } | ConvertTo-Json } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint ='https://api.twitter.com/2/users/{0}/pinned_lists/{1}' -f $BluebirdPSConfiguration.AuthUserId,$List.Id } } try{ $SetTwitterPinnedList = Invoke-TwitterRequest -RequestParameters $Request $PinnedList = $SetTwitterPinnedList ? 'pinned' : 'not pinned' 'List {0} is {1}' -f $List.ToShortString(),$PinnedList } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Test-TwitterListMembership { [OutputType('System.Boolean')] [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [BluebirdPS.APIV2.UserInfo.User]$User ) if (-Not $PSBoundParameters.ContainsKey('User')) { $User = Get-TwitterUser } $UserLists = Get-TwitterListMembership -User $User $List.Id -in $UserLists.Id } function Test-TwitterListSubscription { [OutputType('System.Boolean')] [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.ListInfo.List]$List, [BluebirdPS.APIV2.UserInfo.User]$User ) if (-Not $PSBoundParameters.ContainsKey('User')) { $User = Get-TwitterUser } $UserSubscriptions = Get-TwitterListSubscription -User $User $List.Id -in $UserSubscriptions.Id } function Get-Tweet { [OutputType( 'BluebirdPS.APIV2.TweetInfo.Tweet', 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.MediaInfo.Media', 'BluebirdPS.APIV2.Objects.Poll' )] [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string[]]$Id, [switch]$IncludeExpansions, [ValidateRange(10,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics ) $Request = [TwitterRequest]@{ ExpansionType = 'Tweet' NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics IncludeExpansions = $IncludeExpansions } if ($Id.Count -gt 1) { $Request.Query.Add('ids',($Id -join ',')) $Request.Endpoint = 'https://api.twitter.com/2/tweets' } else { $Request.Endpoint = 'https://api.twitter.com/2/tweets/{0}' -f $Id } Invoke-TwitterRequest -RequestParameters $Request } function Get-TweetConversation { [OutputType( 'BluebirdPS.APIV2.TweetInfo.Tweet', 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.MediaInfo.Media', 'BluebirdPS.APIV2.Objects.Poll' )] [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet, [switch]$IncludeExpansions, [ValidateRange(10,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics ) # The initial Get-Tweet needs to include the switch parameters that are present if ($PSCmdlet.ParameterSetName -eq 'ById') { $Tweet = Get-Tweet -Id $Id } if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } if ($Tweet.CreatedAt -lt (Get-Date).AddDays(-7)) { 'As searching by ConversationId is based on recent search from the Standard product track, you can only retreive a conversation that started within the last 7 days.' | Write-Warning return } $Tweet $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/tweets/search/recent' Query = @{ 'query' = ('conversation_id:{0}' -f $Id) } ExpansionType = 'Tweet' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics } Invoke-TwitterRequest -RequestParameters $Request } function Get-TweetCount { [CmdletBinding(DefaultParameterSetName='Default')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SearchString, [ValidateNotNullOrEmpty()] [ValidateSet('Minute','Hour','Day')] [string]$Granularity = 'Hour', [ValidateNotNullOrEmpty()] [datetime]$StartTime, [ValidateNotNullOrEmpty()] [datetime]$EndTime, [ValidateNotNullOrEmpty()] [string]$SinceId, [ValidateNotNullOrEmpty()] [string]$UntilId, [Parameter(Mandatory,ParameterSetName='Summary')] [switch]$Summary, [Parameter(Mandatory,ParameterSetName='CountOnly')] [switch]$CountOnly ) $Request = [TwitterRequest]@{ OAuthVersion = 'OAuth2Bearer' Endpoint = 'https://api.twitter.com/2/tweets/counts/recent' Query = @{ query = $SearchString granularity = $Granularity.ToLower() } } if ($PSBoundParameters.ContainsKey('StartTime')) { $Request.Query.Add('start_time',[Helpers]::ConvertToV1Date($StartTime)) } if ($PSBoundParameters.ContainsKey('EndTime')) { $Request.Query.Add('end_time',[Helpers]::ConvertToV1Date($EndTime)) } if ($PSBoundParameters.ContainsKey('SinceId')) { $Request.Query.Add('since_id',$SinceId) } if ($PSBoundParameters.ContainsKey('UntilId')) { $Request.Query.Add('until_id',$UntilId) } $TweetCount = Invoke-TwitterRequest -RequestParameters $Request $TotalCount = $global:BluebirdPSLastResponse.ApiResponse.meta.total_tweet_count $TweetCountSummary = [TweetInfo.TweetCountSummary]@{ SearchString = $SearchString Granularity = $Granularity StartTime = ($TweetCount.Start | Select-Object -First 1) EndTime = ($TweetCount.End | Select-Object -Last 1) TotalCount = $TotalCount } switch ($PSCmdlet.ParameterSetName) { 'Summary' { $TweetCountSummary } 'CountOnly' { $TotalCount } default { $TweetCount } } } function Get-TweetLikes { [OutputType( 'BluebirdPS.APIV2.UserInfo.User' )] [CmdLetBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet, [switch]$IncludeExpansions ) switch ($PSCmdlet.ParameterSetName) { 'ById' { $TweetId = $Id } 'ByTweet' { $TweetId = $Tweet.Id } } $Request = [TwitterRequest]@{ ExpansionType = 'User' Endpoint = 'https://api.twitter.com/2/tweets/{0}/liking_users' -f $TweetId IncludeExpansions = $IncludeExpansions } Invoke-TwitterRequest -RequestParameters $Request } function Get-TweetPoll { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ById')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ByTweet')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet ) switch ($PSCmdlet.ParameterSetName) { 'ById' { $TweetId = $Id } 'ByTweet' { $TweetId = $Tweet.Id } } Get-Tweet -Id $TweetId -IncludeExpansions | Where-Object { $_.psobject.TypeNames -contains 'BluebirdPS.APIV2.Objects.Poll' } } function Get-TwitterTimeline { [CmdletBinding(DefaultParameterSetName='User')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='User')] [ValidateSet('Retweets','Replies')] [string[]]$Exclude, [Parameter(Mandatory,ParameterSetName='Mentions')] [switch]$Mentions, [ValidateNotNullOrEmpty()] [datetime]$StartTime, [ValidateNotNullOrEmpty()] [datetime]$EndTime, [ValidateNotNullOrEmpty()] [string]$SinceId, [ValidateNotNullOrEmpty()] [string]$UntilId, [switch]$IncludeExpansions, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics, [ValidateRange(10,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Query = @{ 'max_results' = $MaxResultsPerPage } ExpansionType = 'Tweet' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics } switch ($PSCmdlet.ParameterSetName) { 'User' { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/tweets' -f $User.Id if ($PSBoundParameters.ContainsKey('Exclude')){ $Request.Query.Add('exclude', ($Exclude.ToLower() -join ',') ) } } 'Mentions' { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/mentions' -f $User.Id } } if ($PSBoundParameters.ContainsKey('StartTime')) { $Request.Query.Add('start_time',[Helpers]::ConvertToV1Date($StartTime)) } if ($PSBoundParameters.ContainsKey('EndTime')) { $Request.Query.Add('end_time',[Helpers]::ConvertToV1Date($EndTime)) } if ($PSBoundParameters.ContainsKey('SinceId')) { $Request.Query.Add('since_id',$SinceId) } if ($PSBoundParameters.ContainsKey('UntilId')) { $Request.Query.Add('until_id',$UntilId) } Invoke-TwitterRequest -RequestParameters $Request } function Publish-Tweet { [CmdletBinding(DefaultParameterSetName = 'Tweet')] param( [Parameter(Mandatory, ValueFromPipeline, Position = '1')] [ValidateLength(1, 10000)] [string]$TweetText, [Parameter()] [string]$ReplyToTweet, [Parameter()] [string]$QuoteTweet, [Parameter(ParameterSetName = 'Tweet')] [string[]]$MediaId, [Parameter(Mandatory, ParameterSetName = 'TweetWithMedia')] [ValidateScript({ Test-Path -Path $_ })] [string]$Path, [Parameter(Mandatory, ParameterSetName = 'TweetWithMedia')] [ValidateSet('TweetImage', 'TweetVideo', 'TweetGif')] [string]$Category, [Parameter(ParameterSetName = 'TweetWithMedia')] [ValidateLength(1, 1000)] [string]$AltImageText ) # https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update # maximum of 4 pics, or 1 gif, or 1 video # count $TweetText characters # if the count is greater than allowed, suggest Send-TweetThread and fail if ($PSCmdlet.ParameterSetName -eq 'TweetWithMedia') { $SendMediaParams = @{ Path = $Path Category = $Category } if ($PSBoundParameters.ContainsKey('AltImageText')) { $SendMediaParams.Add('AltImageText', $AltImageText) } $MediaId = Send-TwitterMedia @SendMediaParams | Select-Object -ExpandProperty media_id } $Body = @{ text = $TweetText } if ($PSBoundParameters.ContainsKey('ReplyToTweet')) { $Reply = @{ in_reply_to_tweet_id = $ReplyToTweet } $Body.Add('reply', $Reply) } if ($PSBoundParameters.ContainsKey('QuoteTweet')) { $Body.Add('quote_tweet_id', $QuoteTweet) } if ($MediaId.Count -gt 0) { $Media = @{ media_ids = $MediaId } $Body.Add('media', $Media) } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/tweets' ContentType = 'application/json' Body = ($Body | Convertto-Json -Depth 10 -Compress) } try { $Tweet = Invoke-TwitterRequest -RequestParameters $Request Get-Tweet -Id $Tweet.id } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Search-Tweet { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SearchString, [switch]$IncludeExpansions, [ValidateRange(10,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination, [switch]$NonPublicMetrics, [switch]$PromotedMetrics, [switch]$OrganicMetrics ) if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/tweets/search/recent' Query = @{ 'query' = $SearchString 'max_results' = $MaxResultsPerPage } ExpansionType = 'Tweet' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination NonPublicMetrics = $NonPublicMetrics PromotedMetrics = $PromotedMetrics OrganicMetrics = $OrganicMetrics } Invoke-TwitterRequest -RequestParameters $Request } function Set-Retweet { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName = 'Retweet')] param( [Parameter(Mandatory)] [string]$Id, [Parameter(ParameterSetName = 'Retweet')] [switch]$Retweet, [Parameter(ParameterSetName = 'Unretweet')] [switch]$Unretweet ) if ($PSCmdlet.ParameterSetName -eq 'Retweet') { $Body = @{ tweet_id = $Id } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/retweets' -f $BluebirdPSConfiguration.AuthUserId Body = ($Body | ConvertTo-Json -Depth 10 -Compress) } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/retweets/{1}' -f $BluebirdPSConfiguration.AuthUserId, $id } } Invoke-TwitterRequest -RequestParameters $Request } function Set-TweetLike { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='LikeById')] param( [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeById')] [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeById')] [ValidateNotNullOrEmpty()] [ValidateCount(1,100)] [ValidatePattern('^[0-9]{1,19}$', ErrorMessage = "The tweet Id '{0}' is not valid.")] [string]$Id, [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='LikeByTweet')] [Parameter(Mandatory,ValueFromPipeline,ParameterSetName='UnlikeByTweet')] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet, [Parameter(ParameterSetName='LikeById')] [Parameter(ParameterSetName='LikeByTweet')] [switch]$Like, [Parameter(Mandatory,ParameterSetName='UnlikeById')] [Parameter(Mandatory,ParameterSetName='UnlikeByTweet')] [switch]$Unlike ) if ($PSCmdlet.ParameterSetName -match 'Id$') { $TweetId = $Id } else { $TweetId = $Tweet.Id } if ($PSCmdlet.ParameterSetName -match '^Like') { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/likes' -f $BluebirdPSConfiguration.AuthUserId Body = '{{"tweet_id": "{0}"}}' -f $TweetId } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/likes/{1}' -f $BluebirdPSConfiguration.AuthUserId,$TweetId } } Invoke-TwitterRequest -RequestParameters $Request } function Set-TweetReply { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='Hide')] param( [Parameter(Mandatory)] [string]$Id, [Parameter(ParameterSetName='Hide')] [switch]$Hide, [Parameter(ParameterSetName='Show')] [switch]$Show ) switch ($PSCmdlet.ParameterSetName) { 'Hide' { $Body = '{"hidden": true}' } 'Show' { $Body = '{"hidden": false}' } } $Request = [TwitterRequest]@{ HttpMethod = 'PUT' Endpoint = 'https://api.twitter.com/2/tweets/{0}/hidden' -f $Id Body = $Body } Invoke-TwitterRequest -RequestParameters $Request } function Unpublish-Tweet { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByTweet',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.TweetInfo.Tweet]$Tweet ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $TweetId = $Id $TweetInfo = 'Id: {0}' -f $Id } else { $TweetId = $Tweet.Id $TweetInfo = 'Id: {0}, CreatedAt: {1}' -f $Tweet.Id,$Tweet.CreatedAt } if ($PSCmdlet.ShouldProcess($TweetInfo, 'Deleting Tweet')) { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/tweets/{0}' -f $TweetId } Invoke-TwitterRequest -RequestParameters $Request | Out-Null } } function Add-TwitterFriend { [CmdletBinding(DefaultParameterSetName='ById')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $User = Get-TwitterUser -Id $Id } $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId Body = '{{"target_user_id": "{0}"}}' -f $User.Id } try { $AddTwitterFriend = Invoke-TwitterRequest -RequestParameters $Request $Following = $AddTwitterFriend.following ? 'following' : 'not following' 'You are {0} user {1}' -f $Following,$User.ToString() if ($AddTwitterFriend.pending_follow) { 'There is a pending follow for user {1}' -f $User.ToString() } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Get-TwitterBlockedUser { [OutputType('BluebirdPS.APIV2.UserInfo.User')] [CmdletBinding()] param( [switch]$IncludeExpansions, [ValidateRange(1,1000)] [int]$MaxResultsPerPage=1000, [switch]$NoPagination ) if ($MaxResultsPerPage -lt 1000) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterFollowers { [OutputType('BluebirdPS.APIV2.UserInfo.User')] [CmdletBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions, [ValidateRange(1,1000)] [int]$MaxResultsPerPage=1000, [switch]$NoPagination ) switch ($PSCmdlet.ParameterSetName) { 'ById' { if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $Id } else { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $BluebirdPSConfiguration.AuthUserId } } 'ByUser' { $Endpoint = 'https://api.twitter.com/2/users/{0}/followers' -f $User.Id } } if ($MaxResultsPerPage -lt 1000) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = $Endpoint Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterFriends { [OutputType('BluebirdPS.APIV2.UserInfo.User')] [CmdletBinding(DefaultParameterSetName='ById')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User, [switch]$IncludeExpansions, [ValidateRange(1,1000)] [int]$MaxResultsPerPage=1000, [switch]$NoPagination ) switch ($PSCmdlet.ParameterSetName) { 'ById' { if ($PSBoundParameters.ContainsKey('Id')) { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $Id } else { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $BluebirdPSConfiguration.AuthUserId } } 'ByUser' { $Endpoint = 'https://api.twitter.com/2/users/{0}/following' -f $User.Id } } if ($MaxResultsPerPage -lt 1000) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = $Endpoint Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterMutedUser { [OutputType('BluebirdPS.APIV2.UserInfo.User')] [OutputType('BluebirdPS.APIV2.TweetInfo.Tweet')] [CmdletBinding()] param( [switch]$IncludeExpansions, [ValidateRange(1,100)] [int]$MaxResultsPerPage=100, [switch]$NoPagination ) if ($MaxResultsPerPage -lt 100) { $NoPagination = $true } $Request = [TwitterRequest]@{ Endpoint = 'https://api.twitter.com/2/users/{0}/muting' -f $BluebirdPSConfiguration.AuthUserId Query = @{'max_results' = $MaxResultsPerPage } ExpansionType = 'User' IncludeExpansions = $IncludeExpansions NoPagination = $NoPagination } Invoke-TwitterRequest -RequestParameters $Request } function Get-TwitterUser { [OutputType( 'BluebirdPS.APIV2.UserInfo.User', 'BluebirdPS.APIV2.TweetInfo.Tweet' )] [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [ValidateCount(1, 100)] [string[]]$User, [switch]$IncludeExpansions ) begin { $UserNames = [List[string]]::new() $UserIds = [List[string]]::new() } process { foreach ($ThisUser in $User) { try { [long]::Parse($ThisUser) | Out-Null $UserIds.Add($ThisUser) } catch { $UserNames.Add($ThisUser) } } } end { if ($UserNames.Count -eq 0 -and $UserIds.Count -eq 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions Endpoint = 'https://api.twitter.com/2/users/me' } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } if ($UserNames.Count -gt 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions } if ($UserNames.Count -eq 1) { $Request.Endpoint = 'https://api.twitter.com/2/users/by/username/{0}' -f $UserNames[0] } else { $Request.Endpoint = 'https://api.twitter.com/2/users/by' $Request.Query = @{'usernames' = $UserNames -join ',' } } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } if ($UserIds.Count -gt 0) { $Request = [TwitterRequest]@{ ExpansionType = 'User' IncludeExpansions = $IncludeExpansions } if ($UserIds.Count -eq 1) { $Request.Endpoint = 'https://api.twitter.com/2/users/{0}' -f $UserIds[0] } else { $Request.Endpoint = 'https://api.twitter.com/2/users' $Request.Query = @{'ids' = $UserIds -join ',' } } $Request.SetCommandName('Get-TwitterUser') Invoke-TwitterRequest -RequestParameters $Request } } } function Remove-TwitterFriend { [CmdletBinding(DefaultParameterSetName='ById',SupportsShouldProcess,ConfirmImpact='High')] param( [Parameter(Mandatory,ParameterSetName='ById',ValueFromPipeline)] [string]$Id, [Parameter(Mandatory,ParameterSetName='ByUser',ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User ) if ($PSCmdlet.ParameterSetName -eq 'ById') { $UserId = $Id $User = Get-TwitterList -Id $Id } else { $UserId = $User.Id } $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/following/{1}' -f $BluebirdPSConfiguration.AuthUserId,$UserId } try{ if ($PSCmdlet.ShouldProcess($User.ToString(), 'Unfollow user')) { $RemoveTwitterFriend = Invoke-TwitterRequest -RequestParameters $Request $Following = $RemoveTwitterFriend ? 'following' : 'not following' 'You are {0} user {1}' -f $Following,$User.ToString() } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-TwitterBlockedUser { [CmdletBinding(DefaultParameterSetName='Block')] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateObjectNotNullOrEmpty()] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='Block')] [switch]$Block, [Parameter(Mandatory,ParameterSetName='Unblock')] [switch]$Unblock ) if ($PSCmdlet.ParameterSetName -eq 'Block') { $Request = [TwitterRequest]@{ HttpMethod = 'POST' Endpoint = 'https://api.twitter.com/2/users/{0}/blocking' -f $BluebirdPSConfiguration.AuthUserId Body = '{{"target_user_id": "{0}"}}' -f $User.Id } } else { $Request = [TwitterRequest]@{ HttpMethod = 'DELETE' Endpoint = 'https://api.twitter.com/2/users/{0}/blocking/{1}' -f $BluebirdPSConfiguration.AuthUserId,$User.Id } } try{ $SetTwitterBlockedUser = Invoke-TwitterRequest -RequestParameters $Request $Blocking = $SetTwitterBlockedUser ? 'blocking' : 'not blocking' 'You are {0} user {1}' -f $Blocking,$User.ToString() } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Set-TwitterMutedUser { [CmdletBinding(DefaultParameterSetName='Mute')] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.APIV2.UserInfo.User]$User, [Parameter(ParameterSetName='Mute')] [switch]$Mute, [Parameter(Mandatory,ParameterSetName='Unmute')] [switch]$Unmute ) $Request = [TwitterRequest]@{} if ($PSCmdlet.ParameterSetName -eq 'Mute') { $Request.HttpMethod = 'POST' $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/muting' -f $BluebirdPSConfiguration.AuthUserId $Request.Body = [PSCustomObject]@{ 'target_user_id' = $User.Id } | ConvertTo-Json } else { $Request.HttpMethod = 'DELETE' $Request.Endpoint = 'https://api.twitter.com/2/users/{0}/muting/{1}' -f $BluebirdPSConfiguration.AuthUserId,$User.Id } try { $SetTwitterMutedUser = Invoke-TwitterRequest -RequestParameters $Request $Muting = $SetTwitterMutedUser ? 'muting' : 'not muting' 'You are {0} user {1}' -f $Muting,$User.ToString() } catch { $PSCmdlet.ThrowTerminatingError($_) } } function ConvertFrom-EpochTime { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$UnixTime ) if ($UnixTime.Length -eq 10) { [DateTimeOffset]::FromUnixTimeSeconds([long]::Parse($UnixTime)).ToLocalTime().DateTime } else { [DateTimeOffset]::FromUnixTimeMilliseconds([long]::Parse($UnixTime)).ToLocalTime().DateTime } } function ConvertFrom-TwitterV1Date { [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Date ) try { [datetime]::ParseExact( $Date, "ddd MMM dd HH:mm:ss zzz yyyy", [CultureInfo]::InvariantCulture ) } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Export-BluebirdPSConfiguration { [CmdletBinding()] param() try { if (-Not (Test-Path -Path $BluebirdPSConfiguration.ConfigurationPath)) { $Action = 'new' New-Item -Path $BluebirdPSConfiguration.ConfigurationPath -Force -ItemType File | Out-Null } else { $Action = 'existing' } if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { $BluebirdPSConfiguration.AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime } $BluebirdPSConfiguration | ConvertTo-Json | Set-Content -Path $BluebirdPSConfiguration.ConfigurationPath -Force 'Saved BluebirdPS Configuration to {0} file: {1}' -f $Action,$BluebirdPSConfiguration.ConfigurationPath | Write-Verbose } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Find-TwitterMastodonLinks { [OutputType('BluebirdPS.TwitterMastodonReference')] [CmdletBinding()] param( [Parameter(Mandatory,ValueFromPipeline)] [BluebirdPS.TwitterObject[]]$TwitterObject, [ValidateNotNullOrEmpty()] [Alias('IgnoreUrl')] [string[]]$IgnoreDomain ) begin { $MastodonUserAccountRegex = '(?<MastodonUser>@\S+)@(?<MastodonInstance>[\w-.]+)' $MastodonUserUrlRegex = '(?<MastodonInstance>[\w-.]+\w+(\/web)*)\/(?<MastodonUser>@[\w+]+)' $MastodonUsers = [System.Collections.Generic.List[TwitterMastodonReference]]::new() $TwitterUsers = [System.Collections.Generic.List[User]]::new() $Tweets = [System.Collections.Generic.List[Tweet]]::new() if ($PSBoundParameters.ContainsKey('IgnoreDomain')) { $IgnoreDomainRegex = $IgnoreDomain -join '|' } else { $IgnoreDomainRegex = $false } } process { foreach ($Object in $TwitterObject) { if ($Object.GetType() -match 'User') { $TwitterUsers.Add($Object) } if ($Object.GetType() -match 'Tweet') { $Tweets.Add($Object) } } } end { foreach ($User in $TwitterUsers) { if ($User.Name -match $MastodonUserAccountRegex) { if ($IgnoreDomainRegex) { if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) { continue } } $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'Name') if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) { $MastodonUsers.Add($MastodonUser) } } if ($User.Description -match $MastodonUserAccountRegex) { if ($IgnoreDomainRegex) { if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) { continue } } $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'Description') if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) { $MastodonUsers.Add($MastodonUser) } } foreach ($Url in ($User.Entities.Where{$_.GetType() -match 'Url'} )) { if ($Url.ToString() -match $MastodonUserUrlRegex) { if ($IgnoreDomainRegex) { if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) { continue } } $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'UrlEntity') if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) { $MastodonUsers.Add($MastodonUser) } } } } foreach ($Tweet in $Tweets) { $User = $TwitterUsers | Where-Object Id -eq $Tweet.AuthorId | Select-Object -First 1 if ($Tweet.Text -match $MastodonUserAccountRegex) { if ($IgnoreDomainRegex) { if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) { continue } } $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'TweetText') if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) { $MastodonUsers.Add($MastodonUser) } } foreach ($Url in ($Tweet.Entities.Where{$_.GetType() -match 'Url'})) { if ($Url.ToString() -match $MastodonUserUrlRegex) { if ($IgnoreDomainRegex) { if ($Matches['MastodonInstance'] -match $IgnoreDomainRegex) { continue } } $MastodonUser = [TwitterMastodonReference]::new($User,$Matches,'TweetUrlEntity') if (-Not $MastodonUsers.Contains($MastodonUser) -and $MastodonUser.IsValidDomain) { $MastodonUsers.Add($MastodonUser) } } } $User = $null } $MastodonUsers | Sort-Object -Property TwitterUserName } } function Get-BluebirdPSAssemblyDetails { [OutputType('System.Reflection.TypeInfo')] param() ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {$_.Location -match 'bluebirdps'}).GetTypes() | Where-Object {$_.Namespace -and $_.Fullname -notmatch '\+'} | Sort-Object -Property Namespace,Fullname } function Get-BluebirdPSConfiguration { [OutputType('BluebirdPS.Configuration')] [CmdletBinding()] param() $BluebirdPSConfiguration } function Get-BluebirdPSHistory { [OutputType('BluebirdPS.ResponseData')] [CmdletBinding()] param( [ValidateRange(1,[int]::MaxValue)] [int]$First, [ValidateRange(1,[int]::MaxValue)] [int]$Last, [ValidateRange(1,[int]::MaxValue)] [int]$Skip, [ValidateRange(1,[int]::MaxValue)] [int]$SkipLast, [switch]$Errors ) $SelectObjectParams = @{} foreach ($Key in $PSBoundParameters.Keys) { if ($Key -notin [Cmdlet]::CommonParameters -and $Key -ne 'Errors') { $SelectObjectParams.Add($Key,$PSBoundParameters[$Key]) } } if ($Errors.IsPresent) { $SelectObjectParams.Add( 'Property', @( 'Command', 'Status' @{l='Errors';e= { if ($_.ApiResponse.Errors.Detail) { $_.ApiResponse.Errors.Detail } elseif ($_.ApiResponse.Errors.Message) { $_.ApiResponse.Errors.Message } }} ) ) } $BluebirdPSHistoryList | Select-Object @SelectObjectParams } function Get-BluebirdPSVersion { [CmdletBinding()] param() $BluebirdPSVersion } function Get-TwitterApiEndpoint { [OutputType('Get-TwitterApiEndpoint')] [CmdletBinding()] param( [Parameter()] [string[]]$CommandName, [ValidateNotNullOrEmpty()] [string]$Endpoint ) if ($PSBoundParameters.ContainsKey('Endpoint')) { $TwitterEndpoints | Where-Object {$_.ApiEndpoint -match $Endpoint } } elseif ($PSBoundParameters.ContainsKey('CommandName')) { $TwitterEndpoints | Where-Object {$_.CommandName -in $CommandName} } else { $TwitterEndpoints } } function Import-BluebirdPSConfiguration { [CmdletBinding()] param() $FileDescription = 'BluebirdPS configuration file' 'Checking {0}.' -f $FileDescription | Write-Verbose if (Test-Path -Path $BluebirdPSConfiguration.ConfigurationPath) { '{0} found.' -f $FileDescription | Write-Verbose try { 'Attempting to import {0}.' -f $FileDescription | Write-Verbose $ConfigFromDisk = Get-Content -Path $BluebirdPSConfiguration.ConfigurationPath | ConvertFrom-Json # ensure that the configuration file has the correct keys/attributes $ConfigObject = [Configuration]@{} foreach ($ConfigValue in $ConfigObject.psobject.Properties.Name) { if ($ConfigValue -eq 'AuthLastExportDate') { if ($null -ne $ConfigFromDisk.AuthLastExportDate) { $AuthLastExportDate = $ConfigFromDisk.AuthLastExportDate 'Importing value {0} into {1}' -f $AuthLastExportDate,$ConfigValue | Write-Verbose } else { if (Test-Path -Path $BluebirdPSConfiguration.CredentialsPath) { $AuthLastExportDate = (Get-ChildItem -Path $BluebirdPSConfiguration.CredentialsPath).LastWriteTime 'Discovered value {0} from LastWriteTime for {1}' -f $AuthLastExportDate,$ConfigValue | Write-Verbose } } $BluebirdPSConfiguration.AuthLastExportDate = $AuthLastExportDate continue } # Deprecated configuration properties that were not moved to BluebirdPS Profile if ($ConfigValue -in 'RawOutput') { 'Configuration property {0} has been removed. Please see documentation for further details.' -f $ConfigValue | Write-Warning continue } if ($null -ne $ConfigFromDisk.$ConfigValue) { 'Importing value {0} into {1}' -f $ConfigFromDisk.$ConfigValue,$ConfigValue | Write-Verbose $BluebirdPSConfiguration.$ConfigValue = $ConfigFromDisk.$ConfigValue } } $BluebirdPSRateLimitAction = 'env:BLUEBIRDPS_RATE_LIMIT_ACTION' if (Test-Path -Path $BluebirdPSRateLimitAction) { if ($env:BLUEBIRDPS_RATE_LIMIT_ACTION -in [enum]::GetNames([BluebirdPS.RateLimitAction])) { if ($BluebirdPSConfiguration.RateLimitAction -eq $env:BLUEBIRDPS_RATE_LIMIT_ACTION) { 'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. The value {0} is the same as the currently saved value.' -f $BluebirdPSConfiguration.RateLimitAction | Write-Verbose } else { 'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. Overriding RateLimitAction value: {0} (current), {1} (override).' -f $BluebirdPSConfiguration.RateLimitAction,$env:BLUEBIRDPS_RATE_LIMIT_ACTION | Write-Verbose $BluebirdPSConfiguration.RateLimitAction = $env:BLUEBIRDPS_RATE_LIMIT_ACTION } } else { 'Discovered environment variable BLUEBIRDPS_RATE_LIMIT_ACTION. The value {0} is not valid.' -f $env:BLUEBIRDPS_RATE_LIMIT_ACTION | Write-Warning } } '{0} imported.' -f $FileDescription | Write-Verbose } catch { '{0} appears to be corrupted. Please run Export-BluebirdPSConfiguration to regenerate.' -f $FileDescription | Write-Warning } } } function Set-BluebirdPSConfiguration { [CmdletBinding()] param( [BluebirdPS.RateLimitAction]$RateLimitAction, [int]$RateLimitThreshold, [BluebirdPS.OutputType]$OutputType, [switch]$Export ) $ConfigParameters = $PSBoundParameters.Keys.Where{ $_ -notin [Cmdlet]::CommonParameters -and $_ -ne 'Export' } foreach ($Config in $ConfigParameters) { 'Setting configuration value for {0} to {1}' -f $Key,$PSBoundParameters[$Config] | Write-Verbose $BluebirdPSConfiguration.$Config = $PSBoundParameters[$Config] } if ($Export.IsPresent) { Export-BluebirdPSConfiguration } else { 'Use the -Export switch to save the new configuration to disk.' | Write-Verbose } } #region Configuration and Authentication if (-Not (Test-Path -Path $DefaultSavePath)) { # on first module import, create default save path and export configuration # import authentication will instruct user to run Set-TwiterAuthentication New-Item -Path $DefaultSavePath -Force -ItemType Directory | Out-Null Export-BluebirdPSConfiguration Import-TwitterAuthentication } else { # after first module import, import configuration and authentication Import-BluebirdPSConfiguration Import-TwitterAuthentication } #end region #region Get-TwitterApiEndpoint setup # register arugment completers Register-ArgumentCompleter -CommandName Get-TwitterApiEndpoint -ParameterName CommandName -ScriptBlock { param($commandName,$parameterName,$stringMatch) Get-Command -Module BluebirdPS -ListImported | ForEach-Object Name | Where-Object { $_ -match $stringMatch } } # store EndpointInfo in module variable $BluebirdPSCommands = Get-Command -Module BluebirdPS -ListImported [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $script:TwitterEndpoints = foreach ($Command in $BluebirdPSCommands) { $NavigationLinks = (Get-Help -Name $Command.Name).relatedLinks.navigationLink.Where{$_.linkText -match '^(?!.*(Online|\w+-)).*$'}.Where{$_.linkText -match '- \w+\s(\/|\w+\/)'} if ($NavigationLinks.Count -gt 0) { $ApiEndpoint = $NavigationLinks.LinkText | ForEach-Object { $_.Split('-')[1].Trim() } $ApiDocumentation = $NavigationLinks.Uri } else { continue } [EndpointInfo]::new( $Command.Name, $ApiEndpoint, $ApiDocumentation ) } #endregion #region BluebirdPS Version $ModuleManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'BluebirdPS.psd1' [SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] $BluebirdPSVersion = (Import-PowerShellDataFile -Path $ModuleManifestPath).ModuleVersion #endregion |