├── .gitignore ├── images ├── module_icon.png ├── screenshot.png └── pushover_logo.png ├── src ├── Classes │ ├── PoshoverInformationLevel.ps1 │ ├── MessagePriority.ps1 │ ├── PoshoverUserValidation.ps1 │ └── PoshoverNotificationStatus.ps1 ├── Tests │ └── PoshOver.Tests.ps1 ├── Private │ ├── ConvertTo-PlainText.ps1 │ ├── Save-PushoverConfig.ps1 │ ├── Import-PushoverConfig.ps1 │ └── Send-MessageWithAttachment.ps1 ├── Public │ ├── Get-PushoverConfig.ps1 │ ├── Reset-PushoverConfig.ps1 │ ├── Wait-Pushover.ps1 │ ├── Get-PushoverSound.ps1 │ ├── Set-PushoverConfig.ps1 │ ├── Get-PushoverStatus.ps1 │ ├── Test-PushoverUser.ps1 │ └── Send-Pushover.ps1 ├── Poshover.psm1 ├── Poshover.Format.ps1xml └── Poshover.psd1 ├── debug.ps1 ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── CODE_OF_CONDUCT.md ├── psakefile.ps1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | output/ -------------------------------------------------------------------------------- /images/module_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshooaj/Poshover/HEAD/images/module_icon.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshooaj/Poshover/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /images/pushover_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshooaj/Poshover/HEAD/images/pushover_logo.png -------------------------------------------------------------------------------- /src/Classes/PoshoverInformationLevel.ps1: -------------------------------------------------------------------------------- 1 | enum PoshoverInformationLevel { 2 | Detailed 3 | Quiet 4 | } -------------------------------------------------------------------------------- /src/Classes/MessagePriority.ps1: -------------------------------------------------------------------------------- 1 | enum MessagePriority { 2 | Lowest = -2 3 | Low = -1 4 | Normal = 0 5 | High = 1 6 | Emergency = 2 7 | } -------------------------------------------------------------------------------- /src/Classes/PoshoverUserValidation.ps1: -------------------------------------------------------------------------------- 1 | class PoshoverUserValidation { 2 | [bool]$Valid 3 | [bool]$IsGroup 4 | [string[]]$Devices 5 | [string[]]$Licenses 6 | [string]$Error 7 | } -------------------------------------------------------------------------------- /src/Classes/PoshoverNotificationStatus.ps1: -------------------------------------------------------------------------------- 1 | class PoshoverNotificationStatus { 2 | [string]$Receipt 3 | [bool]$Acknowledged 4 | [datetime]$AcknowledgedAt 5 | [string]$AcknowledgedBy 6 | [string]$AcknowledgedByDevice 7 | [datetime]$LastDeliveredAt 8 | [bool]$Expired 9 | [datetime]$ExpiresAt 10 | [bool]$CalledBack 11 | [datetime]$CalledBackAt 12 | } -------------------------------------------------------------------------------- /src/Tests/PoshOver.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Module Manifest Tests' { 2 | BeforeAll { 3 | $ModuleManifestName = 'Poshover.psd1' 4 | $script:ModuleManifestPath = "$PSScriptRoot\..\$ModuleManifestName" 5 | } 6 | 7 | It 'Passes Test-ModuleManifest' { 8 | Test-ModuleManifest -Path $script:ModuleManifestPath | Should -Not -BeNullOrEmpty 9 | $? | Should -Be $true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Private/ConvertTo-PlainText.ps1: -------------------------------------------------------------------------------- 1 | function ConvertTo-PlainText { 2 | [CmdletBinding()] 3 | param ( 4 | # Specifies a securestring value to decrypt back to a plain text string 5 | [Parameter(Mandatory, ValueFromPipeline)] 6 | [securestring] 7 | $Value 8 | ) 9 | 10 | process { 11 | ([pscredential]::new('unused', $Value)).GetNetworkCredential().Password 12 | } 13 | } -------------------------------------------------------------------------------- /debug.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Used as a launch script in VSCode to... 3 | - Remove all previous builds sitting in the output folder 4 | - Build the module using the psakefile.ps1 build task 5 | - Find the module manifest and force import it 6 | 7 | This makes for a quick developer "inner loop" 8 | #> 9 | 10 | Get-ChildItem $PSScriptRoot\output -ErrorAction Ignore | Remove-Item -Recurse -Force 11 | Remove-Module -Name Poshover* -Force -ErrorAction Ignore 12 | Invoke-psake build 13 | $manifest = Get-ChildItem -Path $PSScriptRoot\output\*.psd1 -Recurse 14 | Import-Module $manifest.FullName -Force -------------------------------------------------------------------------------- /src/Private/Save-PushoverConfig.ps1: -------------------------------------------------------------------------------- 1 | function Save-PushoverConfig { 2 | <# 3 | .SYNOPSIS 4 | Save module configuration to disk 5 | #> 6 | [CmdletBinding()] 7 | param () 8 | 9 | process { 10 | Write-Verbose "Saving the module configuration to '$($script:configPath)'" 11 | $directory = ([io.fileinfo]$script:configPath).DirectoryName 12 | if (-not (Test-Path -Path $directory)) { 13 | $null = New-Item -Path $directory -ItemType Directory -Force 14 | } 15 | $script:config | Export-Clixml -Path $script:configPath -Force 16 | } 17 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Public/Get-PushoverConfig.ps1: -------------------------------------------------------------------------------- 1 | function Get-PushoverConfig { 2 | <# 3 | .SYNOPSIS 4 | Get the Pushover configuration from the Poshover module 5 | .DESCRIPTION 6 | Properties like the API URI and default application and user tokens can be read and written 7 | using Get-PushoverConfig and Set-PushoverConfig. 8 | #> 9 | [CmdletBinding()] 10 | param () 11 | 12 | process { 13 | [pscustomobject]@{ 14 | PSTypeName = 'Poshover.PushoverConfig' 15 | ApiUri = $script:config.PushoverApiUri 16 | Token = $script:config.DefaultAppToken 17 | User = $script:config.DefaultUserToken 18 | ConfigPath = $script:configPath 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Public/Reset-PushoverConfig.ps1: -------------------------------------------------------------------------------- 1 | function Reset-PushoverConfig { 2 | <# 3 | .SYNOPSIS 4 | Reset Poshover module's configuration to default values 5 | #> 6 | [CmdletBinding(SupportsShouldProcess)] 7 | param () 8 | 9 | process { 10 | if ($PSCmdlet.ShouldProcess("Poshover Module Configuration", "Reset to default")) { 11 | Write-Verbose "Using the default module configuration" 12 | $script:config = @{ 13 | PushoverApiDefaultUri = 'https://api.pushover.net/1' 14 | PushoverApiUri = 'https://api.pushover.net/1' 15 | DefaultApplicationToken = $null 16 | DefaultUserToken = $null 17 | } 18 | Save-PushoverConfig 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/Private/Import-PushoverConfig.ps1: -------------------------------------------------------------------------------- 1 | function Import-PushoverConfig { 2 | <# 3 | .SYNOPSIS 4 | Imports the configuration including default API URI's and tokens 5 | .DESCRIPTION 6 | If the module has been previously used, the configuration should be present. If the config 7 | can be imported, the function returns true. Otherwise it returns false. 8 | #> 9 | [CmdletBinding()] 10 | [OutputType([bool])] 11 | param () 12 | 13 | process { 14 | if (Test-Path -Path $script:configPath) { 15 | try { 16 | Write-Verbose "Importing configuration from '$($script:configPath)'" 17 | $script:config = Import-Clixml -Path $script:configPath 18 | return $true 19 | } 20 | catch { 21 | Write-Error "Failed to import configuration from '$script:configPath'." -Exception $_.Exception 22 | } 23 | } 24 | else { 25 | Write-Verbose "No existing module configuration found at '$($script:configPath)'" 26 | } 27 | $false 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Josh Hendricks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Poshover.psm1: -------------------------------------------------------------------------------- 1 | $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction Ignore -Recurse ) 2 | $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction Ignore -Recurse ) 3 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction Ignore -Recurse ) 4 | 5 | foreach ($import in $Classes + $Public + $Private) { 6 | try { 7 | . $import.FullName 8 | } 9 | catch { 10 | Write-Error "Failed to import file $($import.FullName): $_" 11 | } 12 | } 13 | 14 | $script:PushoverApiDefaultUri = 'https://api.pushover.net/1' 15 | $script:PushoverApiUri = $script:PushoverApiDefaultUri 16 | 17 | $script:configPath = Join-Path $env:APPDATA 'Poshover\config.xml' 18 | $script:config = $null 19 | if (-not (Import-PushoverConfig)) { 20 | Reset-PushoverConfig 21 | } 22 | 23 | $soundsCompleter = { 24 | param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) 25 | 26 | $soundList = @('incoming', 'pianobar', 'climb', 'gamelan', 'bugle', 'vibrate', 'pushover', 'cosmic', 'spacealarm', 'updown', 'none', 'persistent', 'cashregister', 'mechanical', 'bike', 'classical', 'falling', 'alien', 'magic', 'siren', 'tugboat', 'intermission', 'echo') 27 | $soundList | Where-Object { 28 | $_ -like "$wordToComplete*" 29 | } | Foreach-Object { 30 | "'$_'" 31 | } 32 | } 33 | Register-ArgumentCompleter -CommandName Send-Pushover -ParameterName Sound -ScriptBlock $soundsCompleter 34 | 35 | Export-ModuleMember -Function ($Public.BaseName) -------------------------------------------------------------------------------- /src/Public/Wait-Pushover.ps1: -------------------------------------------------------------------------------- 1 | function Wait-Pushover { 2 | <# 3 | .SYNOPSIS 4 | Waits for a user to acknowledge receipt of the Pushover message or for the notification to expire 5 | .DESCRIPTION 6 | Waits for a user to acknowledge receipt of the Pushover message or for the notification to expire 7 | then returns the last [PoshoverNotificationStatus] response object. 8 | .EXAMPLE 9 | PS C:\> Send-Pushover -Message 'Please clap' -MessagePriority Emergency | Wait-Pushover 10 | Sends an emergency Pushover notification and then waits for the notification to expire or for at least one user to acknowledge it. 11 | #> 12 | [CmdletBinding()] 13 | [OutputType([PoshoverNotificationStatus])] 14 | param ( 15 | # Specifies the Pushover application API token/key. 16 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 17 | [Parameter()] 18 | [ValidateNotNullOrEmpty()] 19 | [securestring] 20 | $Token, 21 | 22 | # Specifies the receipt received from emergency notifications sent using Send-Pushover 23 | [Parameter(Mandatory, ValueFromPipeline)] 24 | [string] 25 | $Receipt, 26 | 27 | # Specifies the interval between each Pushover API request for receipt status 28 | [Parameter()] 29 | [ValidateRange(5, 10800)] 30 | [int] 31 | $Interval = 10 32 | ) 33 | 34 | begin { 35 | $config = Get-PushoverConfig 36 | } 37 | 38 | process { 39 | if ($null -eq $Token) { 40 | $Token = $config.Token 41 | if ($null -eq $Token) { 42 | throw "Token not provided and no default application token has been set using Set-PushoverConfig." 43 | } 44 | } 45 | 46 | $timeoutAt = (Get-Date).AddHours(3) 47 | while ((Get-Date) -lt $timeoutAt.AddSeconds($Interval)) { 48 | $status = Get-PushoverStatus -Token $Token -Receipt $Receipt -ErrorAction Stop 49 | $timeoutAt = $status.ExpiresAt 50 | if ($status.Acknowledged -or $status.Expired) { 51 | break 52 | } 53 | Start-Sleep -Seconds $Interval 54 | } 55 | Write-Output $status 56 | } 57 | } -------------------------------------------------------------------------------- /src/Poshover.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pushover Notification Status 6 | 7 | PoshoverNotificationStatus 8 | 9 | 10 | 11 | 12 | 30 13 | 14 | 15 | 30 16 | 17 | 18 | 12 19 | 20 | 21 | 30 22 | 23 | 24 | 20 25 | 26 | 27 | 7 28 | 29 | 30 | 31 | 32 | 33 | 34 | Receipt 35 | 36 | 37 | LastDeliveredAt 38 | 39 | 40 | Acknowledged 41 | 42 | 43 | AcknowledgedAt 44 | 45 | 46 | AcknowledgedByDevice 47 | 48 | 49 | Expired 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Private/Send-MessageWithAttachment.ps1: -------------------------------------------------------------------------------- 1 | function Send-MessageWithAttachment { 2 | <# 3 | .SYNOPSIS 4 | Sends an HTTP POST to the Pushover API using an HttpClient 5 | .DESCRIPTION 6 | When sending an image attachment with a Pushover message, you must use multipart/form-data 7 | and there doesn't seem to be a nice way to do this using Invoke-RestMethod like we're doing 8 | in the public Send-Message function. So when an attachment is provided to Send-Message, the 9 | body hashtable is constructed, and then sent over to this function to keep the main 10 | Send-Message function a manageable size. 11 | #> 12 | [CmdletBinding()] 13 | param ( 14 | # Specifies the various parameters and values expected by the Pushover messages api. 15 | [Parameter(Mandatory)] 16 | [hashtable] 17 | $Body, 18 | 19 | # Specifies the image to attach to the message as a byte array 20 | [Parameter(Mandatory)] 21 | [byte[]] 22 | $Attachment, 23 | 24 | # Optionally specifies a file name to associate with the attachment 25 | [Parameter()] 26 | [string] 27 | $FileName = 'attachment.jpg' 28 | ) 29 | 30 | begin { 31 | $uri = $script:PushoverApiUri + '/messages.json' 32 | } 33 | 34 | process { 35 | try { 36 | $client = [system.net.http.httpclient]::new() 37 | try { 38 | $content = [system.net.http.multipartformdatacontent]::new() 39 | foreach ($key in $Body.Keys) { 40 | $textContent = [system.net.http.stringcontent]::new($Body.$key) 41 | $content.Add($textContent, $key) 42 | } 43 | $jpegContent = [system.net.http.bytearraycontent]::new($Attachment) 44 | $jpegContent.Headers.ContentType = [system.net.http.headers.mediatypeheadervalue]::new('image/jpeg') 45 | $jpegContent.Headers.ContentDisposition = [system.net.http.headers.contentdispositionheadervalue]::new('form-data') 46 | $jpegContent.Headers.ContentDisposition.Name = 'attachment' 47 | $jpegContent.Headers.ContentDisposition.FileName = $FileName 48 | $content.Add($jpegContent) 49 | 50 | Write-Verbose "Message body:`r`n$($content.ReadAsStringAsync().Result.Substring(0, 2000).Replace($Body.token, "********").Replace($Body.user, "********"))" 51 | $result = $client.PostAsync($uri, $content).Result 52 | Write-Output ($result.Content.ReadAsStringAsync().Result | ConvertFrom-Json) 53 | } 54 | finally { 55 | $content.Dispose() 56 | } 57 | } 58 | finally { 59 | $client.Dispose() 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Public/Get-PushoverSound.ps1: -------------------------------------------------------------------------------- 1 | function Get-PushoverSound { 2 | <# 3 | .SYNOPSIS 4 | Gets a hashtable containing the names of sounds available on Pushover, and the description of those sounds 5 | #> 6 | [CmdletBinding()] 7 | [OutputType([hashtable])] 8 | param ( 9 | # Specifies the Pushover application API token/key. 10 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 11 | [Parameter()] 12 | [ValidateNotNullOrEmpty()] 13 | [securestring] 14 | $Token 15 | ) 16 | 17 | begin { 18 | $config = Get-PushoverConfig 19 | $uriBuilder = [uribuilder]($config.ApiUri + '/sounds.json') 20 | } 21 | 22 | process { 23 | if ($null -eq $Token) { 24 | $Token = $config.Token 25 | if ($null -eq $Token) { 26 | throw "Token not provided and no default application token has been set using Set-PushoverConfig." 27 | } 28 | } 29 | 30 | try { 31 | $uriBuilder.Query = "token=" + ($Token | ConvertTo-PlainText) 32 | $response = Invoke-RestMethod -Method Get -Uri $uriBuilder.Uri 33 | } 34 | catch { 35 | Write-Verbose 'Handling HTTP error in Invoke-RestMethod response' 36 | $statusCode = $_.Exception.Response.StatusCode.value__ 37 | Write-Verbose "HTTP status code $statusCode" 38 | if ($statusCode -lt 400 -or $statusCode -gt 499) { 39 | throw 40 | } 41 | 42 | try { 43 | Write-Verbose 'Parsing HTTP request error response' 44 | $stream = $_.Exception.Response.GetResponseStream() 45 | $reader = [io.streamreader]::new($stream) 46 | $response = $reader.ReadToEnd() | ConvertFrom-Json 47 | if ([string]::IsNullOrWhiteSpace($response)) { 48 | throw $_ 49 | } 50 | Write-Verbose "Response body:`r`n$response" 51 | } 52 | finally { 53 | $reader.Dispose() 54 | } 55 | } 56 | 57 | if ($response.status -eq 1) { 58 | $sounds = @{} 59 | foreach ($name in $response.sounds | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) { 60 | $sounds.$name = $response.sounds.$name 61 | } 62 | Write-Output $sounds 63 | } 64 | else { 65 | if ($null -ne $response.error) { 66 | Write-Error $response.error 67 | } 68 | elseif ($null -ne $response.errors) { 69 | foreach ($problem in $response.errors) { 70 | Write-Error $problem 71 | } 72 | } 73 | else { 74 | $response 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/Public/Set-PushoverConfig.ps1: -------------------------------------------------------------------------------- 1 | function Set-PushoverConfig { 2 | <# 3 | .SYNOPSIS 4 | Sets the Pushover configuration in the Poshover module 5 | .DESCRIPTION 6 | The Pushover API URI can be modified for the purpose of test automation, and application 7 | and user tokens can be securely stored on disk so that you don't have to supply the tokens 8 | with every call to Send-Pushover in case you are always sending notifications from the same 9 | application and to the same user/group. 10 | .EXAMPLE 11 | PS C:\> Set-PushoverConfig -Token (Read-Host -AsSecureString) 12 | PS C:\> Set-PushoverConfig -User (Read-Host -AsSecureString) 13 | Reads the desired default application token and user token securely and persists it to disk in the %appdata%/Poshover/config.xml file. 14 | .EXAMPLE 15 | PS C:\> Set-PushoverConfig -ApiUri http://localhost:8888 -Temporary 16 | Sets the Pushover API URI to http://localhost:8888 for the duration of the PowerShell session 17 | or until the Poshover module is forcefully imported again. 18 | #> 19 | [CmdletBinding(SupportsShouldProcess)] 20 | param ( 21 | # Species the base URI to which all HTTP requests should be sent. Recommended to change this only for the purposes of test automation. 22 | [Parameter()] 23 | [uri] 24 | $ApiUri, 25 | 26 | # Specifies the default application api token. If the token parameter is omitted in any Pushover requests, the default will be used. 27 | [Parameter(ParameterSetName = 'AsPlainText')] 28 | [securestring] 29 | $Token, 30 | 31 | # Specifies the default user or group ID string. If the user parameter is omitted in any Pushover requests, the default will be used. 32 | [Parameter()] 33 | [securestring] 34 | $User, 35 | 36 | # Specifies that the new settings should only be temporary and should not be saved to disk. 37 | [Parameter()] 38 | [switch] 39 | $Temporary 40 | ) 41 | 42 | process { 43 | if ($PSBoundParameters.ContainsKey('ApiUri')) { 44 | if ($PSCmdlet.ShouldProcess("Pushover ApiUri", "Set value to '$ApiUri'")) { 45 | $script:config.PushoverAPiUri = $ApiUri.ToString() 46 | } 47 | } 48 | if ($PSBoundParameters.ContainsKey('Token')) { 49 | if ($PSCmdlet.ShouldProcess("Pushover Default Application Token", "Set value")) { 50 | $script:config.DefaultAppToken = $Token 51 | } 52 | } 53 | if ($PSBoundParameters.ContainsKey('User')) { 54 | if ($PSCmdlet.ShouldProcess("Pushover Default User Key", "Set value")) { 55 | $script:config.DefaultUserToken = $User 56 | } 57 | } 58 | 59 | if (-not $Temporary) { 60 | Save-PushoverConfig 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Poshover.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Poshover' 3 | # 4 | # Generated by: Josh Hendricks 5 | # 6 | # Generated on: 5/24/2021 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Poshover.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.1.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'ea845a09-1278-42f2-a90d-a67c08761fc9' 22 | 23 | # Author of this module 24 | Author = 'Josh Hendricks' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Milestone Systems' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2021 Josh Hendricks. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Enables easy integration with the Pushover.net API from PowerShell' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | PowerShellVersion = '5.1' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | FormatsToProcess = @('.\Poshover.Format.ps1xml') 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = '*' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | Prerelease = 'alpha' 98 | 99 | # Tags applied to this module. These help with module discovery in online galleries. 100 | Tags = @('notification', 'pushover', 'mobile', 'message') 101 | 102 | # A URL to the license for this module. 103 | LicenseUri = 'https://github.com/jhendricks123/Poshover/raw/main/LICENSE' 104 | 105 | # A URL to the main website for this project. 106 | ProjectUri = 'https://github.com/jhendricks123/Poshover' 107 | 108 | # A URL to an icon representing this module. 109 | IconUri = 'https://github.com/jhendricks123/Poshover/raw/main/images/module_icon.png' 110 | 111 | # ReleaseNotes of this module 112 | # ReleaseNotes = '' 113 | 114 | } # End of PSData hashtable 115 | 116 | } # End of PrivateData hashtable 117 | 118 | # HelpInfo URI of this module 119 | # HelpInfoURI = '' 120 | 121 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 122 | # DefaultCommandPrefix = '' 123 | 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/Public/Get-PushoverStatus.ps1: -------------------------------------------------------------------------------- 1 | function Get-PushoverStatus { 2 | <# 3 | .SYNOPSIS 4 | Gets the status of a Pushover notification using the receipt from Send-Pushover 5 | .DESCRIPTION 6 | When sending a Pushover notification with MessagePriority of 'Emergency', a receipt 7 | is returned. This receipt is a random string associated with the notification and 8 | can be used to check if and when the notification was delivered and acknowledged, or 9 | if it has expired and is no longer causing notifications to be sent to the user(s). 10 | 11 | When the notification is acknowledged, the user and device performing the acknowledgement 12 | will be included in the returned [PoshoverNotificationStatus] response. 13 | .EXAMPLE 14 | PS C:\> $receipt = Send-Pushover -Message 'Are we there yet?' -MessagePriority Emergency -Sound tugboat 15 | PS C:\> Get-PushoverStatus -Receipt $receipt 16 | Sends an emergency Pushover message and then uses the receipt to check the status of that notification. 17 | #> 18 | [CmdletBinding()] 19 | [OutputType([PoshoverNotificationStatus])] 20 | param ( 21 | # Specifies the Pushover application API token/key. 22 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 23 | [Parameter()] 24 | [ValidateNotNullOrEmpty()] 25 | [securestring] 26 | $Token, 27 | 28 | # Specifies the receipt received from emergency notifications sent using Send-Pushover 29 | [Parameter(Mandatory, ValueFromPipeline)] 30 | [string] 31 | $Receipt 32 | ) 33 | 34 | begin { 35 | $config = Get-PushoverConfig 36 | $uriBuilder = [uribuilder]($config.ApiUri + '/receipts') 37 | } 38 | 39 | process { 40 | if ($null -eq $Token) { 41 | $Token = $config.Token 42 | if ($null -eq $Token) { 43 | throw "Token not provided and no default application token has been set using Set-PushoverConfig." 44 | } 45 | } 46 | $uriBuilder.Path += "/$Receipt.json" 47 | $uriBuilder.Query = "token=" + ($Token | ConvertTo-PlainText) 48 | try { 49 | $uriBuilder.Query = "token=" + ($Token | ConvertTo-PlainText) 50 | $response = Invoke-RestMethod -Method Get -Uri $uriBuilder.Uri 51 | } 52 | catch { 53 | Write-Verbose 'Handling HTTP error in Invoke-RestMethod response' 54 | $statusCode = $_.Exception.Response.StatusCode.value__ 55 | Write-Verbose "HTTP status code $statusCode" 56 | if ($statusCode -lt 400 -or $statusCode -gt 499) { 57 | throw 58 | } 59 | 60 | try { 61 | Write-Verbose 'Parsing HTTP request error response' 62 | $stream = $_.Exception.Response.GetResponseStream() 63 | $reader = [io.streamreader]::new($stream) 64 | $response = $reader.ReadToEnd() | ConvertFrom-Json 65 | if ([string]::IsNullOrWhiteSpace($response)) { 66 | throw $_ 67 | } 68 | Write-Verbose "Response body:`r`n$response" 69 | } 70 | finally { 71 | $reader.Dispose() 72 | } 73 | } 74 | 75 | if ($response.status -eq 1) { 76 | [PoshoverNotificationStatus]@{ 77 | Receipt = $Receipt 78 | Acknowledged = [bool]$response.acknowledged 79 | AcknowledgedAt = [datetimeoffset]::FromUnixTimeSeconds($response.acknowledged_at).DateTime.ToLocalTime() 80 | AcknowledgedBy = $response.acknowledged_by 81 | AcknowledgedByDevice = $response.acknowledged_by_device 82 | LastDeliveredAt = [datetimeoffset]::FromUnixTimeSeconds($response.last_delivered_at).DateTime.ToLocalTime() 83 | Expired = [bool]$response.expired 84 | ExpiresAt = [datetimeoffset]::FromUnixTimeSeconds($response.expires_at).DateTime.ToLocalTime() 85 | CalledBack = [bool]$response.called_back 86 | CalledBackAt = [datetimeoffset]::FromUnixTimeSeconds($response.called_back_at).DateTime.ToLocalTime() 87 | } 88 | } 89 | else { 90 | if ($null -ne $response.error) { 91 | Write-Error $response.error 92 | } 93 | elseif ($null -ne $response.errors) { 94 | foreach ($problem in $response.errors) { 95 | Write-Error $problem 96 | } 97 | } 98 | else { 99 | $response 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/Public/Test-PushoverUser.ps1: -------------------------------------------------------------------------------- 1 | function Test-PushoverUser { 2 | <# 3 | .SYNOPSIS 4 | Test a given user key and optionally device name to see if it is valid according to the Pushover API 5 | .DESCRIPTION 6 | If you are collecting user key's, you may want to verify that the key is valid before accepting it. Use 7 | this cmdlet to test the key to see if it is in fact valid. 8 | 9 | Similar to the Test-NetConnection cmdlet, this can return detailed information or it can return a simple 10 | boolean value. The detailed information can be used to provide a better error message such as 'device name is not valid for user'. 11 | .EXAMPLE 12 | PS C:\> if ($null -eq (Get-PushoverConfig).Token) { Set-PushoverConfig -Token (Read-Host -Prompt 'Pushover Application Token' -AsSecureString) } 13 | PS C:\> Test-PushoverUser -User (Read-Host -Prompt 'Pushover User Key' -AsSecureString) 14 | Checks whether the current user's Pushover config includes a default application token. If not, request the user to enter the application token 15 | and save it for future use. Then request the Pushover user key and test whether the key is valid. 16 | #> 17 | [CmdletBinding()] 18 | [OutputType([PoshoverUserValidation])] 19 | param ( 20 | # Specifies the application API token/key from which the Pushover notification should be sent. 21 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 22 | [Parameter()] 23 | [ValidateNotNullOrEmpty()] 24 | [securestring] 25 | $Token, 26 | 27 | # Specifies the User or Group identifier to which the Pushover message should be sent. 28 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 29 | [Parameter()] 30 | [ValidateNotNullOrEmpty()] 31 | [securestring] 32 | $User, 33 | 34 | # Optionally specifies the device on the user account to validate 35 | [Parameter()] 36 | [ValidateNotNullOrEmpty()] 37 | [string] 38 | $Device, 39 | 40 | # Specifies the information level desired in the response. Quiet means a boolean will be returned while Detailed will return an object with more information. 41 | [Parameter()] 42 | [PoshoverInformationLevel] 43 | $InformationLevel = [PoshoverInformationLevel]::Detailed 44 | ) 45 | 46 | begin { 47 | $config = Get-PushoverConfig 48 | $uri = $config.ApiUri + '/users/validate.json' 49 | } 50 | 51 | process { 52 | if ($null -eq $Token) { 53 | $Token = $config.Token 54 | if ($null -eq $Token) { 55 | throw "Token not provided and no default application token has been set using Set-PushoverConfig." 56 | } 57 | } 58 | if ($null -eq $User) { 59 | $User = $config.User 60 | if ($null -eq $User) { 61 | throw "User not provided and no default user id has been set using Set-PushoverConfig." 62 | } 63 | } 64 | 65 | $body = [ordered]@{ 66 | token = $Token | ConvertTo-PlainText 67 | user = $User | ConvertTo-PlainText 68 | device = $Device 69 | } 70 | 71 | try { 72 | $bodyJson = $body | ConvertTo-Json 73 | Write-Verbose "Message body:`r`n$($bodyJson.Replace($Body.token, "********").Replace($Body.user, "********"))" 74 | $response = Invoke-RestMethod -Method Post -Uri $uri -Body $bodyJson -ContentType application/json -UseBasicParsing 75 | } 76 | catch { 77 | Write-Verbose 'Handling HTTP error in Invoke-RestMethod response' 78 | $statusCode = $_.Exception.Response.StatusCode.value__ 79 | Write-Verbose "HTTP status code $statusCode" 80 | if ($statusCode -lt 400 -or $statusCode -gt 499) { 81 | throw 82 | } 83 | 84 | try { 85 | Write-Verbose 'Parsing HTTP request error response' 86 | $stream = $_.Exception.Response.GetResponseStream() 87 | $reader = [io.streamreader]::new($stream) 88 | $response = $reader.ReadToEnd() | ConvertFrom-Json 89 | if ([string]::IsNullOrWhiteSpace($response)) { 90 | throw $_ 91 | } 92 | Write-Verbose "Response body:`r`n$response" 93 | } 94 | finally { 95 | $reader.Dispose() 96 | } 97 | } 98 | 99 | if ($null -ne $response.status) { 100 | switch ($InformationLevel) { 101 | ([PoshoverInformationLevel]::Quiet) { 102 | Write-Output ($response.status -eq 1) 103 | } 104 | 105 | ([PoshoverInformationLevel]::Detailed) { 106 | [PoshoverUserValidation]@{ 107 | Valid = $response.status -eq 1 108 | IsGroup = $response.group -eq 1 109 | Devices = $response.devices 110 | Licenses = $response.licenses 111 | Error = $response.errors | Select-Object -First 1 112 | } 113 | } 114 | Default { throw "InformationLevel $InformationLevel not implemented." } 115 | } 116 | } 117 | else { 118 | Write-Error "Unexpected response: $($response | ConvertTo-Json)" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /psakefile.ps1: -------------------------------------------------------------------------------- 1 | function Update-ModuleVersion { 2 | <# 3 | .SYNOPSIS 4 | Updates the ModuleVersion parameter for the module manifest 5 | .DESCRIPTION 6 | Takes a hashtable representing the module manifest parameters and updates the ModuleVersion 7 | parameter. The Major and Minor values remain the same, but the Patch value will be updated 8 | based on time since epoch. 9 | #> 10 | [CmdletBinding()] 11 | param( 12 | # Module manifest parameter hashtable 13 | [Parameter(Mandatory)] 14 | [hashtable] 15 | $ManifestParams, 16 | # Pass the version back into the pipeline 17 | [Parameter()] 18 | [switch] 19 | $PassThru 20 | ) 21 | $lastVersion = [version]$ManifestParams.ModuleVersion 22 | $ManifestParams.ModuleVersion = [version]::new($lastVersion.Major, $lastVersion.Minor, [int]([datetimeoffset]::UtcNow.ToUnixTimeSeconds() / 60 / 60)) 23 | if ($PassThru) { 24 | $ManifestParams.ModuleVersion 25 | } 26 | } 27 | 28 | function Expand-PSData { 29 | <# 30 | .SYNOPSIS 31 | Pulls the PrivateData.PSData keys and values and attaches them to the root of the ManifestParams hashtable 32 | #> 33 | [CmdletBinding()] 34 | param( 35 | # Module manifest parameter hashtable 36 | [Parameter(Mandatory)] 37 | [hashtable] 38 | $ManifestParams, 39 | # Remove the PrivateData parameter after extracting the PSData properties and attaching them to ManifestParams 40 | [Parameter()] 41 | [switch] 42 | $RemovePrivateData 43 | ) 44 | 45 | foreach ($key in $manifestParams.PrivateData.PSData.Keys) { 46 | $manifestParams.$key = $manifestParams.PrivateData.PSData.$key 47 | } 48 | if ($RemovePrivateData) { 49 | $manifestParams.Remove('PrivateData') 50 | } 51 | } 52 | 53 | function Remove-EmptyParameters { 54 | <# 55 | .SYNOPSIS 56 | Removes any module manifest parameters with a null or empty value 57 | .DESCRIPTION 58 | Update-ModuleManifest or New-ModuleManifest doesn't like it when you supply an empty value 59 | for a parameter. When importing an existing manifest and using that to create or update a 60 | manifest, you need to remove these empty values. 61 | #> 62 | [CmdletBinding()] 63 | param( 64 | # Module manifest parameter hashtable 65 | [Parameter(Mandatory)] 66 | [hashtable] 67 | $ManifestParams 68 | ) 69 | $propertyKeys = $ManifestParams.Keys | Sort-Object 70 | foreach ($key in $propertyKeys) { 71 | if ($null -eq $ManifestParams.$key -or $ManifestParams.$key.Count -eq 0) { 72 | $ManifestParams.Remove($key) 73 | } 74 | } 75 | } 76 | 77 | function Update-FunctionsToExport { 78 | <# 79 | .SYNOPSIS 80 | Updates the FunctionsToExport parameter for a module manifest 81 | .DESCRIPTION 82 | Finds all public functions to be exported based on the BaseName value of all .PS1 files in 83 | the Public folder, recursively, and updates the FunctionsToExport parameter value of the 84 | supplied hashtable. 85 | #> 86 | [CmdletBinding()] 87 | param( 88 | # Module manifest parameter hashtable 89 | [Parameter(Mandatory)] 90 | [hashtable] 91 | $ManifestParams 92 | ) 93 | $manifestParams.FunctionsToExport = @( Get-ChildItem -Path $PSScriptRoot\src\Public\*.ps1 -Recurse | Select-Object -ExpandProperty BaseName ) 94 | } 95 | function Update-ScriptsToProcess { 96 | <# 97 | .SYNOPSIS 98 | Updates the ScriptsToProcess parameter for a module manifest 99 | .DESCRIPTION 100 | Finds all ps1 files in the Classes folder, recursively, and adds them to the 101 | ScriptsToProcess parameter of the manifest to ensure those classes / models are available 102 | in the user's session. 103 | #> 104 | [CmdletBinding()] 105 | param( 106 | # Module manifest parameter hashtable 107 | [Parameter(Mandatory)] 108 | [hashtable] 109 | $ManifestParams 110 | ) 111 | Push-Location -Path $PSScriptRoot\src 112 | $manifestParams.ScriptsToProcess = @( Get-ChildItem -Path $PSScriptRoot\src\Classes\*.ps1 -Recurse | Select-Object -ExpandProperty FullName | Resolve-Path -Relative ) 113 | Pop-Location 114 | } 115 | 116 | properties { 117 | $script:ModuleName = 'Poshover' 118 | $script:CompanyName = 'Milestone Systems' 119 | $script:ModuleVersion = [version]::new() 120 | } 121 | 122 | Task default -Depends Build 123 | 124 | Task Build { 125 | $srcManifestFile = Get-Item -Path $PSScriptRoot\src\*.psd1 | Select-Object -First 1 126 | $manifestParams = Import-PowerShellDataFile -Path $srcManifestFile.FullName 127 | $script:ModuleVersion = Update-ModuleVersion $manifestParams -PassThru 128 | Update-FunctionsToExport $manifestParams 129 | Update-ScriptsToProcess $manifestParams 130 | Expand-PSData $manifestParams -RemovePrivateData 131 | Remove-EmptyParameters $manifestParams 132 | $manifestParams.Copyright = "(c) $((Get-Date).Year) $($script:CompanyName). All rights reserved." 133 | 134 | $outputDirectory = New-Item -Path "$PSScriptRoot\output\$($script:ModuleName)\$($manifestParams.ModuleVersion)" -ItemType Directory -Force 135 | $dstManifest = Join-Path $outputDirectory.FullName "$($script:ModuleName).psd1" 136 | Get-ChildItem -Path $PSScriptRoot\src | Copy-Item -Destination $outputDirectory.FullName -Recurse -Force 137 | New-ModuleManifest -Path $dstManifest -ModuleVersion $manifestParams.ModuleVersion 138 | Update-ModuleManifest -Path $dstManifest @manifestParams 139 | } 140 | 141 | Task Test -Depends Build { 142 | $manifestPath = "$PSScriptRoot\output\$($script:ModuleName)\$($script:ModuleVersion)\$($script:ModuleName).psd1" 143 | $moduleDirectory = ([IO.FileInfo]$manifestPath).DirectoryName 144 | try { 145 | Push-Location $moduleDirectory 146 | Import-Module -Name $manifestPath -Force 147 | $testResults = Invoke-Pester -Path .\Tests -PassThru 148 | if ($testResults.FailedCount -gt 0) { 149 | Write-Error "Failed $($testResults.FailedCount) tests. Build failed." 150 | } 151 | Invoke-ScriptAnalyzer -Path .\ -Recurse 152 | } 153 | finally { 154 | Pop-Location 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Public/Send-Pushover.ps1: -------------------------------------------------------------------------------- 1 | function Send-Pushover { 2 | <# 3 | .SYNOPSIS 4 | Sends a message to the Pushover API 5 | .EXAMPLE 6 | PS C:\> Send-PushoverMessage -Token $token -User $user -Title 'What time is it?' -Message 'It''s time for lunch' 7 | Sends a notification to the user or group specified in the $user string variable, from the application designed by the application API token value in $token 8 | .EXAMPLE 9 | PS C:\> Send-PushoverMessage -Token $token -User $user -Title 'What time is it?' -Message 'It''s time for lunch' -MessagePriority Emergency -RetryInterval (New-TimeSpan -Seconds 60) -ExpireAfter (New-TimeSpan -Hours 1) 10 | Sends the same notification as Example 1, except with emergency priority which results in the notification being repeated every 60 seconds, until an hour has passed or the message has been acknowledged. 11 | .OUTPUTS 12 | Returns a receipt string if the MessagePriority value was 'Emergency' (2) 13 | #> 14 | [CmdletBinding()] 15 | param ( 16 | # Specifies the application API token/key from which the Pushover notification should be sent. 17 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 18 | [Parameter()] 19 | [ValidateNotNullOrEmpty()] 20 | [securestring] 21 | $Token, 22 | 23 | # Specifies the User or Group identifier to which the Pushover message should be sent. 24 | # Note: The default value will be used if it has been previously set with Set-PushoverConfig 25 | [Parameter()] 26 | [ValidateNotNullOrEmpty()] 27 | [securestring] 28 | $User, 29 | 30 | # Optionally specifies one or more devices to which notifications should be sent. Useful for sending notifications to a targetted device instead of all of the user's devices. 31 | [Parameter()] 32 | [ValidateNotNullOrEmpty()] 33 | [string[]] 34 | $Device, 35 | 36 | # Specifies the title of the Pushover notification. The default will be the application name configured for the application API token supplied. 37 | [Parameter()] 38 | [string] 39 | $Title, 40 | 41 | # Specifies the message to be sent with the Pushover notification. 42 | [Parameter(Mandatory)] 43 | [ValidateNotNullOrEmpty()] 44 | [string] 45 | $Message, 46 | 47 | # Optionally specifies an image in bytes to be attached to the message. 48 | [Parameter()] 49 | [byte[]] 50 | $Attachment, 51 | 52 | # Optionally specifies the file name to associate with the attachment. 53 | [Parameter()] 54 | [string] 55 | $FileName = 'attachment.jpg', 56 | 57 | # Optionally specifies a supplementary URL associated with the message. 58 | [Parameter()] 59 | [ValidateNotNullOrEmpty()] 60 | [uri] 61 | $Url, 62 | 63 | # Optionally specifies a title for the supplementary URL if specified. 64 | [Parameter()] 65 | [ValidateNotNullOrEmpty()] 66 | [string] 67 | $UrlTitle, 68 | 69 | # Parameter help description 70 | [Parameter()] 71 | [MessagePriority] 72 | $MessagePriority, 73 | 74 | # Specifies the interval between emergency Pushover notifications. Pushover will retry until the message is acknowledged, or expired. Valid only with MessagePriority of 'Emergency'. 75 | [Parameter()] 76 | [ValidateScript({ 77 | if ($_.TotalSeconds -lt 30) { 78 | throw 'RetryInterval must be at least 30 seconds' 79 | } 80 | if ($_.TotalSeconds -gt 10800) { 81 | throw 'RetryInterval cannot exceed maximum ExpireAfter value of 3 hours' 82 | } 83 | $true 84 | })] 85 | [timespan] 86 | $RetryInterval = (New-TimeSpan -Minutes 1), 87 | 88 | # Specifies the amount of time unacknowledged notifications will be retried before Pushover stops sending notifications. Valid only with MessagePriority of 'Emergency'. 89 | [Parameter()] 90 | [ValidateScript({ 91 | if ($_.TotalSeconds -le 30) { 92 | throw 'ExpireAfter must be greater than the minimum RetryInterval value of 30 seconds' 93 | } 94 | if ($_.TotalSeconds -gt 10800) { 95 | throw 'ExpireAfter cannot exceed 3 hours' 96 | } 97 | $true 98 | })] 99 | [timespan] 100 | $ExpireAfter = (New-TimeSpan -Minutes 10), 101 | 102 | # Optionally specifies the timestamp associated with the message. Default is DateTime.Now. 103 | [Parameter()] 104 | [datetime] 105 | $Timestamp = (Get-Date), 106 | 107 | # Optionally specifies the notification sound to use 108 | [Parameter()] 109 | [ValidateNotNullOrEmpty()] 110 | [string] 111 | $Sound, 112 | 113 | # Optionally specifies one or more tags to associate with the Pushover notification. Tags can be used to cancel emergency notifications in bulk. 114 | [Parameter()] 115 | [string[]] 116 | $Tags 117 | ) 118 | 119 | begin { 120 | $config = Get-PushoverConfig 121 | $uri = $config.ApiUri + '/messages.json' 122 | } 123 | 124 | process { 125 | if ($null -eq $Token) { 126 | $Token = $config.Token 127 | if ($null -eq $Token) { 128 | throw "Token not provided and no default application token has been set using Set-PushoverConfig." 129 | } 130 | } 131 | if ($null -eq $User) { 132 | $User = $config.User 133 | if ($null -eq $User) { 134 | throw "User not provided and no default user id has been set using Set-PushoverConfig." 135 | } 136 | } 137 | 138 | $deviceList = if ($null -ne $Device) { 139 | [string]::Join(',', $Device) 140 | } else { $null } 141 | 142 | $tagList = if ($null -ne $Tags) { 143 | [string]::Join(',', $Tags) 144 | } else { $null } 145 | 146 | $body = [ordered]@{ 147 | token = $Token | ConvertTo-PlainText 148 | user = $User | ConvertTo-PlainText 149 | device = $deviceList 150 | title = $Title 151 | message = $Message 152 | url = $Url 153 | url_title = $UrlTitle 154 | priority = [int]$MessagePriority 155 | retry = [int]$RetryInterval.TotalSeconds 156 | expire = [int]$ExpireAfter.TotalSeconds 157 | timestamp = [int]([datetimeoffset]::new($Timestamp).ToUnixTimeMilliseconds() / 1000) 158 | tags = $tagList 159 | sound = $Sound 160 | } 161 | 162 | try { 163 | if ($Attachment.Length -eq 0) { 164 | $bodyJson = $body | ConvertTo-Json 165 | Write-Verbose "Message body:`r`n$($bodyJson.Replace($Body.token, "********").Replace($Body.user, "********"))" 166 | $response = Invoke-RestMethod -Method Post -Uri $uri -Body $bodyJson -ContentType application/json -UseBasicParsing 167 | } 168 | else { 169 | $response = Send-MessageWithAttachment -Body $body -Attachment $Attachment -FileName $FileName 170 | } 171 | } 172 | catch { 173 | Write-Verbose 'Handling HTTP error in Invoke-RestMethod response' 174 | $statusCode = $_.Exception.Response.StatusCode.value__ 175 | Write-Verbose "HTTP status code $statusCode" 176 | if ($statusCode -lt 400 -or $statusCode -gt 499) { 177 | throw 178 | } 179 | 180 | try { 181 | Write-Verbose 'Parsing HTTP request error response' 182 | $stream = $_.Exception.Response.GetResponseStream() 183 | $reader = [io.streamreader]::new($stream) 184 | $response = $reader.ReadToEnd() | ConvertFrom-Json 185 | if ([string]::IsNullOrWhiteSpace($response)) { 186 | throw $_ 187 | } 188 | Write-Verbose "Response body:`r`n$response" 189 | } 190 | finally { 191 | $reader.Dispose() 192 | } 193 | } 194 | 195 | if ($response.status -ne 1) { 196 | if ($null -ne $response.error) { 197 | Write-Error $response.error 198 | } 199 | elseif ($null -ne $response.errors) { 200 | foreach ($problem in $response.errors) { 201 | Write-Error $problem 202 | } 203 | } 204 | else { 205 | $response 206 | } 207 | } 208 | 209 | if ($null -ne $response.receipt) { 210 | Write-Output $response.receipt 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Contributors][contributors-shield]][contributors-url] 3 | [![Forks][forks-shield]][forks-url] 4 | [![Stargazers][stars-shield]][stars-url] 5 | [![Issues][issues-shield]][issues-url] 6 | [![MIT License][license-shield]][license-url] 7 | [![LinkedIn][linkedin-shield]][linkedin-url] 8 | [![Twitter][twitter-shield]][twitter-url] 9 | 10 | 11 | 12 |
13 |

14 | 15 | Logo 16 | 17 | 18 |

Poshover - A PowerShell Module for Pushover

19 | 20 |

21 | Send push notifications to mobile devices and desktops from PowerShell using the Pushover service! 22 |
23 | Explore the docs » 24 |
25 |
26 | Report Bug 27 | · 28 | Request Feature 29 |

30 |

31 | 32 | 33 | 34 | 35 |
36 |

Table of Contents

37 |
    38 |
  1. 39 | About The Project 40 |
  2. 41 |
  3. 42 | Getting Started 43 | 47 |
  4. 48 |
  5. Usage
  6. 49 |
  7. Roadmap
  8. 50 |
  9. Contributing
  10. 51 |
  11. License
  12. 52 |
  13. Contact
  14. 53 |
  15. Acknowledgements
  16. 54 |
55 |
56 | 57 | ## NOTICE 58 | 59 | This repository is no longer maintained. The module has been renamed to joshooaj.PSPushover and a [new repository](https://github.com/joshooaj/PSPushover) has been created. 60 | The new repo has a number of improvements including: 61 | 62 | - Automatic documentation generation, updating, and publishing using PlatyPS, MkDocs, and GitHub Pages 63 | - Automatic building and testing on Linux, Windows, and MacOS 64 | - Automatic publishing of tagged versions to PSGallery after successfully passing tests 65 | - Automatic versioning using nbgv 66 | - Codespaces or devcontainer support 67 | - Built-in demo showing how a GitHub action runs and sends a notification when the repository gets starred 68 | 69 | 70 | ## About The Project 71 | 72 | [![Product Name Screen Shot][product-screenshot]](https://github.com/jhendricks123/poshover) 73 | 74 | If you want to send push notifications to one or more mobile or desktop devices from PowerShell, 75 | that's exactly what this module is about. The module makes use of the Pushover.net notification 76 | service which is _free_ for up to 10k notifications per month! 77 | 78 | The goal of the module is to support as much of the Pushover API as possible. To start with, the 79 | messages endpoint is be supported using the Send-Pushover function, and the most common features 80 | including message priority, retry interval, expiration, and attachment capabilities are available. 81 | 82 | Later, support will be added for additional features like checking receipt status, listing and 83 | specifying notification sounds, and other API areas like subscriptions, groups, licensing, etc. 84 | 85 | 86 | ## Getting Started 87 | 88 | Before you use Poshover and the Pushover API, you need a Pushover account and an application token. 89 | 90 | ### Prerequisites 91 | 92 | You need a Pushover account, a user or group key to which messages will be sent, and an application token to which 93 | 1. Go sign up (for free) on [Pushover.net](https://pushover.net/signup) and confirm your email address. 94 | 2. Copy your __user key__ and save it for later. 95 | 3. Scroll down to __Your Applications__ and click [Create an Application/API Token](https://pushover.net/apps/build). 96 | 4. Give it a name (this will be the title of your push notifications when you don't supply your own title) 97 | 5. Pick an icon. It's optional, but you really want your own icon :) 98 | 6. Read the ToS and check the box, then click __Create Application__ 99 | 7. Save your __API Token/Key__ for later. You're ready to install and click __Back to apps__ or click on the Pushover logo in the title bar 100 | 101 | ### Installation 102 | 103 | Launch an elevated PowerShell session and run the following: 104 | ```powershell 105 | Install-Module -Name Poshover -AllowPrerelease 106 | ``` 107 | 108 | If that failed, you may need to update your PowerShellGet package provider. Try this: 109 | ```powershell 110 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 111 | Install-PackageProvider -Name NuGet -Force 112 | Install-Module -Name PowerShellGet -Force 113 | Install-Module -Name Poshover -AllowPrerelease 114 | ``` 115 | 116 | 117 | ## Usage 118 | 119 | Got your application token and user keys handy? Good! 120 | ```powershell 121 | $token = Read-Host -Prompt 'Application API Token' -AsSecureString 122 | $user = Read-Host -Prompt 'User Key' -AsSecureString 123 | Send-Pushover -Token $token -User $user -Message 'Have a nice day!' 124 | ``` 125 | 126 | Don't want to enter your token and user keys every time? You don't have to! 127 | ```powershell 128 | # This will securely save your token and user keys to %appdata%/Poshover/config.xml 129 | $token = Read-Host -Prompt 'Application API Token' -AsSecureString 130 | $user = Read-Host -Prompt 'User Key' -AsSecureString 131 | Set-PushoverConfig -Token $token -User $user 132 | 133 | Send-Pushover -Message 'You are fantastic!' 134 | ``` 135 | 136 | ## Building 137 | 138 | The structure of the PowerShell module is such that the .\src\ directory contains the module manifest, 139 | .psm1 file, and all other functions/classes/tests necessary. The "build" process takes the content 140 | of the .\src\ directory, copies it to .\output\Poshover\version\, and updates the module manifest to 141 | ensure all classes and functions are exported properly. 142 | 143 | To build, simply run `Invoke-psake build` from the root of the project. And to test, run `Invoke-psake test`. 144 | 145 | I like to also setup VSCode to launch .\debug.ps1 when I press F5. This will clear the .\output\ directory 146 | and call the psake build task, then force-import the updated module from the .\output\ directory. I find this 147 | makes the developer "inner-loop" really quick. 148 | 149 | Also, I recommend using the Microsoft.Powershell.SecretManagement module for storing your api keys. That way 150 | you never enter them in clear text. 151 | 152 | 153 | ## Roadmap 154 | 155 | See the [open issues](https://github.com/jhendricks123/Poshover/issues) for a list of proposed features (and known issues). 156 | 157 | 158 | 159 | ## Contributing 160 | 161 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 162 | 163 | 1. Fork the Project 164 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 165 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 166 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 167 | 5. Open a Pull Request 168 | 169 | 170 | 171 | 172 | ## License 173 | 174 | Distributed under the MIT License. See `LICENSE` for more information. 175 | 176 | 177 | 178 | ## Contact 179 | 180 | Josh Hendricks - [@joshooaj](https://twitter.com/@joshooaj) 181 | 182 | Project Link: [https://github.com/jhendricks123/Poshover](https://github.com/jhendricks123/Poshover) 183 | 184 | 185 | 186 | ## Acknowledgements 187 | 188 | * [othneildrew's Best-README-Template](https://github.com/othneildrew/Best-README-Template) 189 | * [Pushover.net's great documentation](https://pushover.net) 190 | 191 | 192 | 193 | 194 | [contributors-shield]: https://img.shields.io/github/contributors/jhendricks123/poshover.svg?style=for-the-badge 195 | [contributors-url]: https://github.com/jhendricks123/poshover/graphs/contributors 196 | [forks-shield]: https://img.shields.io/github/forks/jhendricks123/poshover.svg?style=for-the-badge 197 | [forks-url]: https://github.com/jhendricks123/poshover/network/members 198 | [stars-shield]: https://img.shields.io/github/stars/jhendricks123/poshover.svg?style=for-the-badge 199 | [stars-url]: https://github.com/jhendricks123/poshover/stargazers 200 | [issues-shield]: https://img.shields.io/github/issues/jhendricks123/poshover.svg?style=for-the-badge 201 | [issues-url]: https://github.com/jhendricks123/poshover/issues 202 | [license-shield]: https://img.shields.io/github/license/jhendricks123/poshover.svg?style=for-the-badge 203 | [license-url]: https://github.com/jhendricks123/poshover/blob/master/LICENSE.txt 204 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 205 | [linkedin-url]: https://www.linkedin.com/in/joshuahendricks/ 206 | [twitter-shield]: https://img.shields.io/badge/-Twitter-black.svg?style=for-the-badge&logo=twitter&colorB=555 207 | [twitter-url]: https://twitter.com/joshooaj 208 | [product-screenshot]: images/screenshot.png 209 | --------------------------------------------------------------------------------