├── .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 |
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 | -
39 | About The Project
40 |
41 | -
42 | Getting Started
43 |
47 |
48 | - Usage
49 | - Roadmap
50 | - Contributing
51 | - License
52 | - Contact
53 | - Acknowledgements
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 |
--------------------------------------------------------------------------------