signal.psm1
# Signal.psm1 # Documentation for signal cli REST API # https://bbernhard.github.io/signal-cli-rest-api/ # # Docker image from bbernhard # bbernhard/signal-cli-rest-api:latest # # Git repository from bbernhard # https://github.com/bbernhard/signal-cli-rest-api # Path for configuration files if ($IsWindows) { $SignalConfigFile = [System.IO.FileInfo]::new((Join-Path $env:LOCALAPPDATA "Signal Module" "SignalConfig.xml")) }elseif ($isLinux -or $IsMacOS) { $SignalConfigFile = [System.IO.FileInfo]::new((Join-Path $HOME ".signalmodule" "SignalConfig.xml")) } else { Write-Host "is Windows: $isWindows" Write-Host "is Linux: $isLinux" Write-Host "is MacOS: $IsMacOS" Write-Error "Not supported operating system" exit(3) } #region SignalConfiguration function <# .SYNOPSIS Creates or overwrites the Signal module configuration file. .DESCRIPTION Stores the REST API URL and the registered sender number in an XML file that is loaded automatically by the module. .PARAMETER SenderNumber Phone number in E.164 format used as sender. .PARAMETER SignalServerURL URL of the signal-cli REST API instance, e.g. http://mysignaldocker.local:8080 .EXAMPLE PS C:\> New-SignalConfiguration -SenderNumber '+491234567890' -SignalServerURL 'http://mysignaldocker.local:8080' Creates the configuration file for the module. #> function New-SignalConfiguration { param ( [Parameter(Mandatory = $true)] [ValidatePattern('\+[1-9]{1}[0-9]{9,12}')] [string]$SenderNumber, [Parameter(Mandatory = $true)] [string]$SignalServerURL ) $SignalConfig = [pscustomobject]@{ "ServerURL" = $SignalServerURL; "RegistredNumber" = $SenderNumber } Export-Clixml -Path $SignalConfigFile.Fullname -InputObject $SignalConfig -Force -NoClobber Import-Module $PSCommandPath -force -DisableNameChecking } <# .SYNOPSIS Returns the current Signal module configuration. .DESCRIPTION Reads the configuration XML file and returns its contents. When no configuration exists, a warning is shown unless -Quiet is used. .PARAMETER Quiet Suppresses warnings when the configuration file is missing. #> function Get-SignalConfiguration { [CmdletBinding(PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] [OutputType([object])] param ( [switch]$Quiet ) if (Test-Path $SignalConfigFile.Fullname) { return import-clixml -Path $SignalConfigFile.Fullname -ErrorAction Stop } if (!$Quiet.IsPresent) { Write-Warning "No configuration file found. Path: $($SignalConfigFile.Fullname)" Write-Warning "Run New-SignalConfiguration -SenderNumber +491223345 -SignalServerURL 'http://mysignaldocker.local:8080'" } } if (!(Test-Path $SignalConfigFile.Fullname)) { Write-Warning "Signal is not configured. Run Get-SignalConfiguration to find out more." } if (!(Test-Path $SignalConfigFile.DirectoryName)) { New-Item -Path $SignalConfigFile.DirectoryName -ItemType Directory -Force } #endregion $SignalConfig = Get-signalConfiguration -Quiet $ImageExtensions = @('aces', 'apng', 'avci', 'avcs', 'avif', 'bmp', 'cgm', 'dpx', 'emf', 'example', 'fits', 'g3fax', 'gif', 'heic', 'heif', 'hej2k', 'ief', 'j2c', 'jaii', 'jais', 'jls', 'jp2', 'jpg','jpeg', 'jph', 'jphc', 'jpm', 'jpx', 'jxl', 'jxr', 'jxrA', 'jxrS', 'jxs', 'jxsc', 'jxsi', 'jxss', 'ktx', 'ktx2', 'naplps', 'png', 'svg+xml', 't38', 'tiff', 'tiff-fx', 'webp', 'wmf') $TextExtensions = @('cache-manifest', 'calendar', 'cql', 'cql-expression', 'cql-identifier', 'css', 'csv', 'csv-schema', 'dns', 'encaprtp', 'enriched', 'example', 'fhirpath', 'flexfec', 'fwdred', 'gff3', 'grammar-ref-list', 'hl7v2', 'html', 'javascript', 'jcr-cnd', 'markdown', 'mizar', 'n3', 'parameters', 'parityfec', 'plain', 'provenance-notation', 'raptorfec', 'RED', 'rfc822-headers', 'richtext', 'rtf', 'rtp-enc-aescm128', 'rtploopback', 'rtx', 'SGML', 'shaclc', 'shex', 'spdx', 'strings', 't140', 'tab-separated-values', 'troff', 'turtle', 'ulpfec', 'uri-list', 'vcard', 'vtt', 'wgsl', 'xml', 'xml-external-parsed-entity') $VideoExtensions = @('3gpp', '3gpp2', '3gpp-tt', 'AV1', 'BMPEG', 'BT656', 'CelB', 'DV', 'encaprtp', 'evc', 'example', 'FFV1', 'flexfec', 'H261', 'H263', 'H263-1998', 'H263-2000', 'H264', 'H264-RCDO', 'H264-SVC', 'H265', 'H266', 'iso.segment', 'jxsv', 'lottie+json', 'matroska', 'matroska-3d', 'mj2', 'MP1S', 'MP2P', 'MP2T', 'mp4', 'MP4V-ES', 'MPV', 'mpeg', 'mpeg4-generic', 'nv', 'ogg', 'parityfec', 'pointer', 'quicktime', 'raptorfec', 'raw', 'rtp-enc-aescm128', 'rtploopback', 'rtx', 'scip', 'smpte291', 'SMPTE292M', 'ulpfec', 'vc1', 'vc2', 'VP8', 'VP9') # Helper function for sending HTTP requests <# .SYNOPSIS Sends an HTTP request to the configured Signal REST API. .DESCRIPTION Wraps Invoke-RestMethod and automatically converts the body to JSON. The function is used internally by all other cmdlets. .PARAMETER Method HTTP method such as GET, POST, PUT or DELETE. .PARAMETER Endpoint API endpoint path beginning with '/'. .PARAMETER Headers Optional hashtable of additional HTTP headers. .PARAMETER Body Hashtable representing the JSON body to send. .EXAMPLE PS C:\> Invoke-SignalApiRequest -Method 'GET' -Endpoint '/v1/accounts' #> function Invoke-SignalApiRequest { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [Parameter(Mandatory = $true)] [string]$Method = 'GET', [Parameter(Mandatory = $true)] [string]$Endpoint, [hashtable]$Headers = $null, [hashtable]$Body = $null ) if (! $SignalConfig.ServerURL) { write-error "Signal is not configured. Run Get-SignalConfiguration to find out more." return } $uri = "{0}{1}" -f $SignalConfig.ServerURL, $Endpoint $Parameters = @{ "StatusCodeVariable" = "StatusCode"; "Method" = $Method; "Uri" = $uri; "ContentType" = 'application/json' } if ($Body) { $Parameters.add("Body", ($Body | ConvertTo-Json -Compress -Depth 10)) } if ($Headers) { $Parameters.Add("Headers", $Headers) } write-verbose ($Parameters | ConvertTo-Json -Depth 10) try { $response = Invoke-RestMethod @Parameters } catch { $StatusCode = $_.Exception.Message.split(": ")[1].trim() $SignalMessage = $_.ErrorDetails.Message | convertfrom-json write-error "HTTP: $StatusCode" write-error $SignalMessage.error return } write-verbose "HTTP: $StatusCode" return $response } # Send message <# .SYNOPSIS Sends a text or attachment to one or more recipients. .DESCRIPTION Calls '/v2/send' on the Signal REST API to deliver a message to phone numbers or groups. Attachments are automatically converted to base64. .PARAMETER Recipients One or more phone numbers or group IDs to send to. .PARAMETER Message Optional text message body. .PARAMETER Path Optional path to a file that will be sent as an attachment. #> function Send-SignalMessage { param ( [Parameter(Mandatory = $true)] [string[]]$Recipients, [Parameter(Mandatory = $false)] [string]$Message, [ValidateScript({test-path $_})] [string]$Path ) $body = @{ number = $SignalConfig.RegistredNumber recipients = $Recipients } if ($Message) { $body.add("message", $Message) $body.Add("text_mode", 'normal') } if ($Path) { $FullFilePath = resolve-path $Path $FileName = ($FullFilePath.path -split "\\")[-1] $FileExt = ($FileName -split ".")[-1] $mimetype = "application/$fileExt" if ($ImageExtensions.contains($FileExt.ToLower())) { $mimetype = "image/$fileExt" } if ($TextExtensions.contains($FileExt.ToLower())) { $mimetype = "text/$fileExt" } if ($VideoExtensions.contains($FileExt.ToLower())) { $mimetype = "video/$fileExt" } $base64string = [Convert]::ToBase64String([IO.File]::ReadAllBytes($FullFilePath.path)) $Base64DataStream = "data:$mimetype;filename=$FileName;base64,$base64string" $body.add("base64_attachments", @($Base64DataStream)) } Invoke-SignalApiRequest -Method 'POST' -Endpoint '/v2/send' -Body $body } # Receive messages <# .SYNOPSIS Listens for incoming messages for the configured number. .DESCRIPTION Opens the websocket endpoint '/v1/receive/<number>' and waits until the specified amount of messages has been received or the exit word is detected. .PARAMETER MessageCount Number of messages to wait for. Defaults to 1. .PARAMETER asObject Return the received messages as PowerShell objects instead of writing them to the console. .PARAMETER ExitWord If this word is received the function stops reading from the websocket. .PARAMETER NoOutput Suppress console output while waiting for messages. #> function Receive-SignalMessage { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [int]$MessageCount = 1, [Parameter(Mandatory = $false)] [switch]$asObject, [Parameter(Mandatory = $false, HelpMessage = 'Stop Zeichenfolge')] [string]$ExitWord, [switch]$NoOutput ) $endpoint = "/v1/receive/{0}" -f [uri]::EscapeDataString($SignalConfig.RegistredNumber) $uri = "{0}{1}" -f $SignalConfig.ServerURL.replace("http:", "ws:"), $endpoint $websocket = [System.Net.WebSockets.ClientWebSocket]::new() $websocket.Options.AddSubProtocol("chat") $ct = [System.Threading.CancellationToken]::None Write-Verbose "Connecting to: $uri" $websocket.ConnectAsync($uri, $ct).Wait() $buffer = New-Object byte[] 4096 $segment = [System.ArraySegment[byte]]::new($buffer) [System.Collections.ArrayList]$IncomingMessages = @() Write-Host "Waiting for message..." while ($IncomingMessages.Count -lt $MessageCount) { try { $result = $websocket.ReceiveAsync($segment, $ct).Result if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { Write-Host "Connection closed by server" break } $msg = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count) $json = $msg.trim() | ConvertFrom-Json #if ($json.envelope.typingMessage.action) { write-host $json.envelope.typingMessage.action} if ($json.envelope.dataMessage.message) { [void]$IncomingMessages.add($json.envelope) if (!$NoOutput.IsPresent) { write-host ("{3}. {0} ({1}): {2}" -f $json.envelope.sourceName, $json.envelope.sourceNumber, $json.envelope.dataMessage.message, $IncomingMessages.Count) if ($json.envelope.dataMessage.message -eq $ExitWord) { Write-Verbose "Exit word found. stopping" break } } } } catch { Write-Warning "Invalid JSON line: $msg" $websocket.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "End", $ct).Wait() $websocket.Dispose() return $msg } } $websocket.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "End", $ct).Wait() $websocket.Dispose() if ($asObject.IsPresent) { return $IncomingMessages } } # Register device <# .SYNOPSIS Registers a new device (phone number) on the Signal network. .DESCRIPTION This function sends a registration to the Signal network for the specified phone number. The registration optionally supports CAPTCHA validation and sending the code via voice call. The procedure is: - The phone number is sent to the Signal network. - Optionally a CAPTCHA token is sent if requested by the server. - By default the code is sent via SMS. With the -UseVoice switch a call can be requested instead. .PARAMETER Number Phone number in international format (e.g. +491234567890) to register with Signal. .PARAMETER Captcha STEP 1: CAPTCHA token for spam protection, required for certain network requests. Obtain the CAPTCHA token by calling the Signal API or by solving a CAPTCHA in the browser. CAPTCHA URL: https://signalcaptchas.org/registration/generate Press F12 and copy the last URL from the console. Copy the entire text after "signalcaptcha://" (e.g. signal-hcaptcha.5fad97...Gef) .PARAMETER UseVoice STEP 1: If specified, the verification code is delivered via voice call instead of SMS. Use this parameter instead of 'Captcha' .PARAMETER Code STEP 2: After the first registration step a code is sent via SMS to the used number. Complete the registration with this code. .EXAMPLE Register-SignalDevice -Number "+491234567890" -Captcha "03AFcWeA..." Step 1: Start the registration with CAPTCHA token and request the verification code via SMS. .EXAMPLE Register-SignalDevice -Number "+491234567890" -UseVoice Or step 1: Start the registration and request the verification code via voice call instead of SMS. Unfortunately this does not work with German numbers. .EXAMPLE Register-SignalDevice -Number "+491234567890" -Code Step 2: Complete the registration for the phone number by submitting the code to Signal. .NOTES This function is part of a PowerShell wrapper for signal-cli and uses the Signal REST API internally. Weitere Infos: https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha #> function Register-SignalDevice { [CmdletBinding(DefaultParameterSetName = 'Step1_C', ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [Parameter(ParameterSetName = 'Step1_C', Mandatory = $true, DontShow = $true)] [Parameter(ParameterSetName = 'Step1_V')] [Parameter(ParameterSetName = 'Step2')] [string]$Number, [Parameter(ParameterSetName = 'Step1_C')] [switch]$Captcha, [Parameter(ParameterSetName = 'Step1_V', DontShow = $true)] [switch]$UseVoice, [Parameter(ParameterSetName = 'Step2')] [int]$Code ) $endpoint = "/v1/register/{0}" -f $SignalConfig.RegistredNumber $body = @{ use_voice = $UseVoice.IsPresent } if ($Captcha.IsPresent) { $CaptchaText = read-host -Prompt "signalcaptacha://[YOUR INPUT]" $body.add("captcha", $CaptchaText) } if ($Code) { $endpoint += "/verify/$code" $body = @{ "pin" = "string" } } Invoke-SignalApiRequest -Method 'POST' -Endpoint $endpoint -Body $body } # Unregister device <# .SYNOPSIS Removes the registration for a phone number from the Signal service. .DESCRIPTION Allows deleting the registration and optionally the entire account on the server. Local data can also be removed. .PARAMETER Number Phone number in international format to unregister. .PARAMETER DeleteAccount If set, the Signal account is permanently deleted on the server. .PARAMETER DeleteLocalData Remove local data for the device as well. #> function Unregister-SignalDevice { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [Parameter(Mandatory = $true)] [string]$Number, [Alias('DA')] [switch]$DeleteAccount, [switch]$DeleteLocalData ) $body = @{ 'delete_account' = $DeleteAccount.IsPresent 'delete_local_data' = $DeleteLocalData.IsPresent } $endpoint = "/v1/unregister/{0}" -f $Number Invoke-SignalApiRequest -Method 'POST' -Endpoint $endpoint -Body $body } # Link device and generate QR code function Link-SignalDevice { <# .SYNOPSIS Links another device to the current account. .DESCRIPTION Calls '/v1/qrcodelink' to generate a QR code that can be scanned by the Signal client on the device you want to link. .PARAMETER DeviceName Name of the new device shown in the linked devices list. #> param ( [Parameter(Mandatory = $true)] [string]$DeviceName ) $encodedName = [uri]::EscapeDataString($DeviceName) $endpoint = "/v1/qrcodelink?device_name=$encodedName" Invoke-SignalApiRequest -Method 'GET' -Endpoint $endpoint } <# .SYNOPSIS Retrieves information about the currently registered account. .DESCRIPTION Calls /v1/accounts on the REST API and returns account metadata, including linked devices. #> function Get-SignalAccount { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] $Endpoint = "/v1/accounts" Invoke-SignalApiRequest -Method 'GET' -Endpoint $endpoint } # List available groups <# .SYNOPSIS Lists Signal groups for the configured account. .DESCRIPTION Retrieves group metadata from '/v1/groups'. When a GroupId is provided only that specific group is returned. .PARAMETER GroupId Optional ID of a group to fetch. If omitted all groups are listed. #> function Get-SignalGroups { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [string]$GroupId ) $Endpoint = "/v1/groups/{0}" -f $SignalConfig.RegistredNumber if ($GroupId) { $endpoint += "/$GroupId" } Invoke-SignalApiRequest -Method 'GET' -Endpoint $endpoint -Verbose:$PSBoundParameters.ContainsKey("Verbose") } # Create a Signal group <# .SYNOPSIS Creates a new Signal group. .DESCRIPTION Sends a POST request to '/v1/groups' to create the group with the given name, members and settings. .PARAMETER Name Name of the new group. .PARAMETER Members Array of phone numbers to add to the group. .PARAMETER Description Optional group description. .PARAMETER ExpirationTime Message expiration time in seconds. Use 0 to disable disappearing messages. #> function New-SignalGroup { param ( [Parameter(Mandatory = $true)] [string]$Name, [string[]]$Members, [string]$Description, [int]$ExpirationTime = 0 ) $body = @{ "description" = $Description "expiration_time" = $ExpirationTime "group_link" = "disabled" "members" = [array]$Members "name" = $Name "permissions" = @{ "add_members" = "only-admins" "edit_group" = "only-admins" } } $Endpoint = "/v1/groups/$($SignalConfig.RegistredNumber)" Invoke-SignalApiRequest -Method 'POST' -Endpoint $Endpoint -Body $body } <# .SYNOPSIS Updates the properties of an existing Signal group. .DESCRIPTION Sends a PUT request to '/v1/groups/<number>/<groupId>' to change group metadata such as name, description, avatar or expiration time. .PARAMETER GroupID Identifier of the group to update. .PARAMETER Name New name for the group. .PARAMETER Description New group description. .PARAMETER ExpirationTime New message expiration time in seconds. .PARAMETER Path Path to an avatar image (JPG, PNG or GIF, max 5 MB). .EXAMPLE PS C:\> Update-SignalGroup -GroupID $id -Name "New Name" Renames the group. #> function Update-SignalGroup { param ( [Parameter(Mandatory = $true)] [string]$GroupID, [string]$Name, [string]$Description, [int]$ExpirationTime, [string]$Path ) $body = @{ } if ($Name) { $body.add("name", $Name) } if ($Description) { $body.add("description", $Description) } if ($ExpirationTime) { $body.add("expiration_time", $ExpirationTime) } if ($Path) { $FullFilePath = resolve-path $path $Avatar = [Convert]::ToBase64String([IO.File]::ReadAllBytes($FullFilePath.path)) $body.add("base64_avatar", $Avatar) } $Endpoint = "/v1/groups/{0}/{1}" -f $SignalConfig.RegistredNumber, $GroupID Invoke-SignalApiRequest -Method 'PUT' -Endpoint $Endpoint -Body $body } <# .SYNOPSIS Deletes a Signal group from the account. .DESCRIPTION Sends a DELETE request to '/v1/groups/<number>/<groupId>' to remove the specified group. .PARAMETER GroupId Identifier of the group to delete. .EXAMPLE PS C:\> Remove-SignalGroups -GroupId $id #> function Remove-SignalGroups { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [Parameter(Mandatory = $true)] [string]$GroupId ) $Endpoint = "/v1/groups/{0}/{1}" -f [uri]::EscapeDataString($SignalConfig.RegistredNumber), [uri]::EscapeDataString($GroupId) Invoke-SignalApiRequest -Method 'DELETE' -Endpoint $Endpoint } # Get information about the REST API <# .SYNOPSIS Retrieves version information of the running Signal REST API. .DESCRIPTION Calls '/v1/about' on the Signal REST API and returns details about the service and the bundled signal-cli version. .EXAMPLE PS C:\> Get-SignalAbout Shows the REST API version. #> function Get-SignalAbout { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param () $Endpoint = '/v1/about' Invoke-SignalApiRequest -Method 'GET' -Endpoint $Endpoint } # List linked devices <# .SYNOPSIS Lists devices linked to the configured account. .DESCRIPTION Calls '/v1/devices/<number>' to return all devices that are associated with the registered phone number. If a DeviceId is provided only that specific device is returned. .PARAMETER DeviceId Optional device identifier to fetch a single device. .EXAMPLE PS C:\> Get-SignalDevices Returns all linked devices. #> function Get-SignalDevices { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [string]$DeviceId ) $Endpoint = '/v1/devices/{0}' -f [uri]::EscapeDataString($SignalConfig.RegistredNumber) if ($DeviceId) { $Endpoint += '/' + [uri]::EscapeDataString($DeviceId) } Invoke-SignalApiRequest -Method 'GET' -Endpoint $Endpoint } # Remove linked device <# .SYNOPSIS Unlinks a device from the Signal account. .DESCRIPTION Sends a DELETE request to '/v1/devices/<number>/<deviceId>' which removes the specified device from the account. .PARAMETER DeviceId Identifier of the device to remove. .EXAMPLE PS C:\> Remove-SignalDevice -DeviceId '123456789' #> function Remove-SignalDevice { [CmdletBinding(ConfirmImpact = 'None', PositionalBinding = $false, SupportsPaging = $false, SupportsShouldProcess = $false)] param ( [Parameter(Mandatory = $true)] [string]$DeviceId ) $Endpoint = '/v1/devices/{0}/{1}' -f [uri]::EscapeDataString($SignalConfig.RegistredNumber), [uri]::EscapeDataString($DeviceId) Invoke-SignalApiRequest -Method 'DELETE' -Endpoint $Endpoint } |