PSDsHook.psm1
#Setup default paths for module in user home dir $script:separator = [IO.Path]::DirectorySeparatorChar switch ($PSVersionTable.PSEdition) { 'Desktop' { $userDir = $env:USERPROFILE } 'Core' { switch ($PSVersionTable.Platform) { 'Win32NT' { $userDir = $env:USERPROFILE } 'Unix' { $userDir = $env:HOME } } } } $script:defaultPsDsDir = (Join-Path -Path $userDir -ChildPath '.psdshook') $script:configDir = "$($defaultPsDsDir)$($separator)configs" class DiscordColor { [int]$DecimalColor = $null [string]$HexColor = [string]::Empty DiscordColor() { $embedColor = 8311585 $this.HexColor = "0x$([Convert]::ToString($embedColor, 16).ToUpper())" $this.DecimalColor = $embedColor } DiscordColor([int]$hex) { $this.DecimalColor = $hex $this.HexColor = "0x$([Convert]::ToString($hex, 16).ToUpper())" } DiscordColor([string]$color) { [int]$embedColor = $null try { $embedColor = $color } catch { switch ($Color) { 'blue' { $embedColor = 4886754 } 'red' { $embedColor = 13632027 } 'orange' { $embedColor = 16098851 } 'yellow' { $embedColor = 16312092 } 'brown' { $embedColor = 9131818 } 'lightGreen' { $embedColor = 8311585 } 'green' { $embedColor = 4289797 } 'pink' { $embedColor = 12390624 } 'purple' { $embedColor = 9442302 } 'black' { $embedColor = 1 } 'white' { $embedColor = 16777215 } 'gray' { $embedColor = 10197915 } default { $embedColor = 1 } } } $this.HexColor = "0x$([Convert]::ToString($embedColor, 16).ToUpper())" $this.DecimalColor = $embedColor } DiscordColor( [int]$r, [int]$g, [int]$b ) { $this.DecimalColor = $this.ConvertFromRgb($r, $g, $b) } [string]ConvertFromHex([string]$hex) { [int]$decimalValue = [Convert]::ToDecimal($hex) return $decimalValue } [string]ConvertFromRgb( [int]$r, [int]$g, [int]$b ) { $hexR = [Convert]::ToString($r, 16).ToUpper() if ($hexR.Length -eq 1) { $hexR = "0$hexR" } $hexG = [Convert]::ToString($g, 16).ToUpper() if ($hexG.Length -eq 1) { $hexG = "0$hexG" } $hexB = [Convert]::ToString($b, 16).ToUpper() if ($hexB.Length -eq 1) { $hexB = "0$hexB" } [string]$hexValue = "0x$hexR$hexG$hexB" $this.HexColor = $HexValue [string]$decimalValue = $this.ConvertFromHex([int]$hexValue) return $decimalValue } [string]ToString() { return $this.DecimalColor } } class DiscordConfig { [string]$HookUrl = [string]::Empty DiscordConfig([string]$configPath) { $this.ImportConfig($configPath) } DiscordConfig( [string]$url, [string]$path ) { $this.HookUrl = $url $this.ExportConfig($path) } [void]ExportConfig([string]$path) { Write-Verbose "Exporting configuration information to -> [$path]" $folderPath = Split-Path -Path $path if (!(Test-Path -Path $folderPath)) { Write-Verbose "Creating folder -> [$folderPath]" New-Item -ItemType Directory -Path $folderPath } $this | ConvertTo-Json | Out-File -FilePath $path } [void]ImportConfig([string]$configPath) { Write-Verbose "Importing configuration from -> [$configPath]" $configSettings = Get-Content -Path $configPath -ErrorAction Stop | ConvertFrom-Json $this.HookUrl = $configSettings.HookUrl } } class DiscordField { [string]$name [string]$value [bool]$inline = $false DiscordField( [string]$name, [string]$value ) { $this.name = $name $this.value = $value } DiscordField( [string]$name, [string]$value, [bool]$inline ) { $this.name = $name $this.value = $value $this.inline = $inline } } class DiscordThumbnail { [string]$url = [string]::Empty [int]$width = $null [int]$height = $null DiscordThumbnail([string]$url) { if ([string]::IsNullOrEmpty($url)) { Write-Error "Please provide a url!" } else { $this.url = $url } } DiscordThumbnail( [int]$width, [int]$height, [string]$url ) { if ([string]::IsNullOrEmpty($url)) { Write-Error "Please provide a url!" } else { $this.url = $url $this.height = $height $this.width = $width } } } class DiscordImage { [string]$url = [string]::Empty [string]$proxyUrl = [string]::Empty [int]$width = $null [int]$height = $null DiscordImage([string]$url) { if ([string]::IsNullOrEmpty($url)) { Write-Error "Please provide a url!" } else { $this.url = $url } } DiscordImage( [string]$url, [string]$proxyUrl ) { if ([string]::IsNullOrEmpty($url) -and [string]::IsNullOrEmpty($proxyUrl)) { Write-Error "Please provide: a url and proxyurl" } else { $this.url = $url $this.proxyUrl = $proxyUrl } } DiscordImage( [string]$url, [string]$proxyUrl, [int]$width, [int]$height ) { if ( [string]::IsNullOrEmpty($url) -and [string]::IsNullOrEmpty($proxyUrl) -and !$width -and !($height) ) { Write-Error "Please provide: a url and proxyurl" } else { $this.url = $url $this.proxyUrl = $proxyUrl $this.height = $height $this.width = $width } } } class DiscordAuthor { [string]$name = [string]::Empty [string]$url = [string]::Empty [string]$icon_url = [string]::Empty [string]$proxy_icon_url = [string]::Empty DiscordAuthor([string]$name) { if ([string]::IsNullOrEmpty($name)) { Write-Error "Please provide a name!" } else { $this.name = $name } } DiscordAuthor( [string]$name, [string]$icon_url ) { if ([string]::IsNullOrEmpty($name)) { Write-Error "Please provide a name and icon url" } else { $this.name = $name $this.'icon_url' = $icon_url } } } class DiscordFooter { [string]$text = [string]::Empty [string]$icon_url = [string]::Empty [string]$proxy_icon_url = [string]::Empty DiscordFooter([string]$text) { if ([string]::IsNullOrEmpty($text)) { Write-Error "Please provide some footer text!" } else { $this.text = $text } } DiscordFooter( [string]$text, [string]$icon_url ) { if ([string]::IsNullOrEmpty($text)) { Write-Error "Please provide some text and an icon url" } else { $this.text = $text $this.'icon_url' = $icon_url } } } class DiscordEmbed { [string]$title = [string]::Empty [string]$description = [string]::Empty [System.Collections.ArrayList]$fields = @() [string]$color = [DiscordColor]::New().ToString() $thumbnail = [string]::Empty $image = [string]::Empty $author = [string]::Empty $footer = [string]::Empty $url = [string]::Empty $timestamp = [string]::Empty DiscordEmbed() { Write-Error "Please provide a title and description (and optionally, a color)!" } DiscordEmbed( [string]$embedTitle, [string]$embedDescription ) { $this.title = $embedTitle $this.description = $embedDescription } DiscordEmbed( [string] $embedTitle, [string] $embedDescription, [DiscordColor]$embedColor ) { $this.title = $embedTitle $this.description = $embedDescription $this.color = $embedColor.ToString() } [void]AddTimeStamp() { $this.timeStamp = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss') } [void]AddTimeStamp([DateTime]$stamp) { $this.timeStamp = $stamp.ToString('yyyy-MM-dd HH:mm:ss') } [void]AddField($field) { if ($field.PsObject.TypeNames[0] -eq 'DiscordField') { Write-Verbose "Adding field to field array!" $this.Fields.Add($field) | Out-Null } else { Write-Error "Did not receive a [DiscordField] object!" } } [void]AddThumbnail($thumbNail) { if ($thumbNail.PsObject.TypeNames[0] -eq 'DiscordThumbnail') { $this.thumbnail = $thumbNail } else { Write-Error "Did not receive a [DiscordThumbnail] object!" } } [void]AddImage($image) { if ($image.PsObject.TypeNames[0] -eq 'DiscordImage') { $this.image = $image } else { Write-Error "Did not receive a [DiscordImage] object!" } } [void]AddAuthor($author) { if ($author.PsObject.TypeNames[0] -eq 'DiscordAuthor') { $this.author = $author } else { Write-Error "Did not receive a [DiscordAuthor] object!" } } [void]AddFooter($footer) { if ($footer.PsObject.TypeNames[0] -eq 'DiscordFooter') { $this.footer = $footer } else { Write-Error "Did not receive a [DiscordFooter] object!" } } [void]WithUrl($url) { if (![string]::IsNullOrEmpty($url)) { $this.url = $url } else { Write-Error "Please provide a url!" } } [void]WithColor([DiscordColor]$color) { $this.color = $color } [System.Collections.ArrayList] ListFields() { return $this.Fields } } class DiscordFile { [string]$FilePath = [string]::Empty [string]$FileName = [string]::Empty [string]$FileTitle = [string]::Empty [System.Net.Http.MultipartFormDataContent]$Content = [System.Net.Http.MultipartFormDataContent]::new() [System.IO.FileStream]$Stream = $null DiscordFile([string]$FilePath) { $this.FilePath = $FilePath $this.FileName = Split-Path $filePath -Leaf $this.fileTitle = $this.FileName.Substring(0,$this.FileName.LastIndexOf('.')) $fileContent = $this.GetFileContent($FilePath) $this.Content.Add($fileContent) } [System.Net.Http.StreamContent]GetFileContent($filePath) { $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open) $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $fileHeader.Name = $this.fileTitle $fileHeader.FileName = $this.FileName $fileContent = [System.Net.Http.StreamContent]::new($fileStream) $fileContent.Headers.ContentDisposition = $fileHeader $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain") $this.stream = $fileStream return $fileContent } } function Invoke-PayloadBuilder { [cmdletbinding()] param( [Parameter( Mandatory )] $PayloadObject ) process { $type = $PayloadObject | Get-Member | Select-Object -ExpandProperty TypeName -Unique switch ($type) { 'DiscordEmbed' { [bool]$createArray = $true #check if array $PayloadObject.PSObject.TypeNames | ForEach-Object { switch ($_) { {$_ -match '^System\.Collections\.Generic\.List.+'} { $createArray = $false } 'System.Array' { $createArray = $false } 'System.Collections.ArrayList' { $createArray = $false } } } if (!$createArray) { $embedArray = New-Object 'System.Collections.Generic.List[PSCustomObject]' $PayloadObject | ForEach-Object { $curObject = $null $populated = $null $returnObject = $null $curObject = $_ $populated = $curObject.PSObject.Properties | Where-Object {$_.Value} | ForEach-Object {$_.Name} $returnObject = $curObject | Select-Object -Property $populated $embedArray.Add($returnObject) | Out-Null } $payload = [PSCustomObject]@{ embeds = $embedArray } } else { $populated = $PayloadObject.PSObject.Properties | Where-Object {$_.Value} | ForEach-Object {$_.Name} $embedArray = New-Object 'System.Collections.Generic.List[PSCustomObject]' $PayloadObject = $PayloadObject | Select-Object -Property $populated $embedArray.Add($PayloadObject) | Out-Null $payload = [PSCustomObject]@{ embeds = $embedArray } } } 'System.String' { if (Test-Path $PayloadObject -ErrorAction SilentlyContinue) { $payload = [DiscordFile]::New($payloadObject) } else { $payload = [PSCustomObject]@{ content = ($PayloadObject | Out-String) } } } } } end { return $payload } } function Invoke-PSDsHook { <# .SYNOPSIS Invoke-PSDsHook Use PowerShell classes to make using Discord Webhooks easy and extensible .DESCRIPTION This function allows you to use Discord Webhooks with embeds, files, and various configuration settings .PARAMETER CreateConfig If specified, will create a configuration file containing the webhook URL as the argument. You can use the ConfigName parameter to create another configuration separate from the default. .PARAMETER WebhookUrl If used with an embed or file, this URL will be used in the webhook call. .PARAMETER ConfigName Specified a name for the configuration file. Can be used when creating a configuration file, as well as when passing embeds/files. .PARAMETER ListConfigs Lists configuration files .PARAMETER EmbedObject Accepts an array of [EmbedObject]'s to pass in the webhook call. .EXAMPLE (Create a configuration file) Configuration files are stored in a sub directory of your user's home directory named .psdshook/configs Invoke-PsDsHook -CreateConfig "www.hook.com/hook" .EXAMPLE (Create a configuration file with a non-standard name) Configuration files are stored in a sub directory of your user's home directory named .psdshook/configs Invoke-PsDsHook -CreateConfig "www.hook.com/hook2" -ConfigName 'config2' .EXAMPLE (Send an embed with the default config) using module PSDsHook If the module is not in one of the folders listed in ($env:PSModulePath -split "$([IO.Path]::PathSeparator)") You must specify the full path to the psm1 file in the above using statement Example: using module 'C:\users\thegn\repos\PsDsHook\out\PSDsHook\0.0.1\PSDsHook.psm1' Create embed builder object via the [DiscordEmbed] class $embedBuilder = [DiscordEmbed]::New( 'title', 'description' ) Add blue color $embedBuilder.WithColor( [DiscordColor]::New( 'blue' ) ) Finally, call the function that will send the embed array to the webhook url via the default configuraiton file Invoke-PSDsHook $embedBuilder -Verbose .EXAMPLE (Send an webhook with just text) Invoke-PSDsHook -HookText 'this is the webhook message' -Verbose #> [cmdletbinding()] param( [Parameter( ParameterSetName = 'createDsConfig' )] [string] $CreateConfig, [Parameter( )] [string] $WebhookUrl, [Parameter( Mandatory, ParameterSetName = 'file' )] [string] $FilePath, [Parameter( )] [string] $ConfigName = 'config', [Parameter( ParameterSetName = 'configList' )] [switch] $ListConfigs, [Parameter( ParameterSetName = 'embed', Position = 0 )] $EmbedObject, [Parameter( ParameterSetName = 'simple' )] [string] $HookText ) begin { #Create full path to the configuration file $configPath = "$($configDir)$($separator)$($ConfigName).json" #Ensure we can access the path, and error out if we cannot if (!(Test-Path -Path $configPath -ErrorAction SilentlyContinue) -and !$CreateConfig -and !$WebhookUrl) { throw "Unable to access [$configPath]. Please provide a valid configuration name. Use -ListConfigs to list configurations, or -CreateConfig to create one." } elseif (!$CreateConfig -and $WebhookUrl) { $hookUrl = $WebhookUrl Write-Verbose "Manual mode enabled..." } elseif ((!$CreateConfig -and !$WebhookUrl) -and $configPath) { #Get configuration information from the file specified $config = [DiscordConfig]::New($configPath) $hookUrl = $config.HookUrl } } process { switch ($PSCmdlet.ParameterSetName) { 'embed' { $payload = Invoke-PayloadBuilder -PayloadObject $EmbedObject Write-Verbose "Sending:" Write-Verbose "" Write-Verbose ($payload | ConvertTo-Json -Depth 4) try { Invoke-RestMethod -Uri $hookUrl -Body ($payload | ConvertTo-Json -Depth 4) -ContentType 'Application/Json' -Method Post } catch { $errorMessage = $_.Exception.Message throw "Error executing Discord Webhook -> [$errorMessage]!" } } 'file' { if ($PSVersionTable.PSVersion.Major -lt 6) { throw "Support for sending files is not yet available in PowerShell 5.x" } else { $fileInfo = Invoke-PayloadBuilder -PayloadObject $FilePath $payload = $fileInfo.Content Write-Verbose "Sending:" Write-Verbose "" Write-Verbose ($payload | Out-String) #If it is a file, we don't want to include the ContentType parameter, as it is included in the body try { Invoke-RestMethod -Uri $hookUrl -Body $payload -Method Post } catch { $errorMessage = $_.Exception.Message throw "Error executing Discord Webhook -> [$errorMessage]!" } finally { $fileInfo.Stream.Dispose() } } } 'simple' { $payload = Invoke-PayloadBuilder -PayloadObject $HookText Write-Verbose "Sending:" Write-Verbose "" Write-Verbose ($payload | ConvertTo-Json -Depth 4) try { Invoke-RestMethod -Uri $hookUrl -Body ($payload | ConvertTo-Json -Depth 4) -ContentType 'Application/Json' -Method Post } catch { $errorMessage = $_.Exception.Message throw "Error executing Discord Webhook -> [$errorMessage]!" } } 'createDsConfig' { [DiscordConfig]::New($CreateConfig, $configPath) } 'configList' { $configs = (Get-ChildItem -Path (Split-Path $configPath) | Where-Object { $PSitem.Extension -eq '.json' } | Select-Object -ExpandProperty Name) if ($configs) { Write-Host "Configuration files in [$configDir]:" return $configs } else { Write-Host "No configuration files found in [$configDir]" } } } } } |