TeamViewerPS.psm1

function ConvertTo-DateTime {
    param(
        [Parameter(ValueFromPipeline)]
        [string]
        $InputString
    )

    process {
        try {
            Write-Output ([DateTime]::Parse($InputString))
        }
        catch {
            Write-Output $null
        }
    }
}


function ConvertTo-ErrorRecord {
    param(
        [Parameter(ValueFromPipeline)]
        [object]
        $InputObject,

        [Parameter()]
        [System.Management.Automation.ErrorCategory]
        $ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
    )
    Process {
        $category = $ErrorCategory
        $message = $InputObject.ToString()
        $errorId = 'TeamViewerError'

        if ($InputObject.PSObject.TypeNames -contains 'TeamViewerPS.RestError') {
            $category = switch ($InputObject.ErrorCategory) {
                'invalid_request' { [System.Management.Automation.ErrorCategory]::InvalidArgument }
                'invalid_token' { [System.Management.Automation.ErrorCategory]::AuthenticationError }
                'internal_error' { [System.Management.Automation.ErrorCategory]::NotSpecified }
                'rate_limit_reached' { [System.Management.Automation.ErrorCategory]::LimitsExceeded }
                'token_expired' { [System.Management.Automation.ErrorCategory]::AuthenticationError }
                'wrong_credentials' { [System.Management.Automation.ErrorCategory]::AuthenticationError }
                'invalid_client' { [System.Management.Automation.ErrorCategory]::InvalidArgument }
                'not_found' { [System.Management.Automation.ErrorCategory]::ObjectNotFound }
                'too_many_retries' { [System.Management.Automation.ErrorCategory]::LimitsExceeded }
                'invalid_permission' { [System.Management.Automation.ErrorCategory]::PermissionDenied }
                default { [System.Management.Automation.ErrorCategory]::NotSpecified }
            }
            $errorId = 'TeamViewerRestError'
        }

        $exception = [System.Management.Automation.RuntimeException]($message)
        $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $category, $null
        $errorRecord.ErrorDetails = $message
        return $errorRecord
    }
}



function ConvertTo-TeamViewerAccount {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Name             = $InputObject.name
            Email            = $InputObject.email
            UserId           = $InputObject.userid
            CompanyName      = $InputObject.company_name
            IsEmailValidated = $InputObject.email_validated
            EmailLanguage    = $InputObject.email_language
        }
        if ($InputObject.email_language -And $InputObject.email_language -Ne 'auto') {
            $properties["EmailLanguage"] = [System.Globalization.CultureInfo]($InputObject.email_language)
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Account')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.Name) <$($this.Email)>"
        }
        Write-Output $result
    }
}



function ConvertTo-TeamViewerContact {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id                = $InputObject.contact_id
            UserId            = $InputObject.user_id
            GroupId           = $InputObject.groupid
            Name              = $InputObject.name
            Description       = $InputObject.description
            OnlineState       = $InputObject.online_state
            ProfilePictureUrl = $InputObject.profilepicture_url
            SupportedFeatures = $InputObject.supported_features
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Contact')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.Name)"
        }
        Write-Output $result
    }
}



function ConvertTo-TeamViewerDevice {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $remoteControlId = $InputObject.remotecontrol_id | `
            Select-String -Pattern 'r(\d+)' | `
            ForEach-Object { $_.Matches.Groups[1].Value }
        $properties = @{
            Id                         = $InputObject.device_id
            TeamViewerId               = $remoteControlId
            GroupId                    = $InputObject.groupid
            Name                       = $InputObject.alias
            Description                = $InputObject.description
            OnlineState                = $InputObject.online_state
            IsAssignedToCurrentAccount = $InputObject.assigned_to
            SupportedFeatures          = $InputObject.supported_features
        }
        if ($InputObject.policy_id) {
            $properties['PolicyId'] = $InputObject.policy_id
        }
        if ($InputObject.last_seen) {
            $properties['LastSeenAt'] = [datetime]($InputObject.last_seen)
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Device')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.Name)"
        }
        Write-Output $result
    }
}



function ConvertTo-TeamViewerGroup {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id          = $InputObject.id
            Name        = $InputObject.name
            Permissions = $InputObject.permissions
            SharedWith  = @($InputObject.shared_with | ConvertTo-TeamViewerGroupShare)
        }
        if ($InputObject.owner) {
            $properties.Owner = [pscustomobject]@{
                UserId = $InputObject.owner.userid
                Name   = $InputObject.owner.name
            }
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Group')
        Write-Output $result
    }
}


function ConvertTo-TeamViewerGroupShare {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            UserId      = $InputObject.userid
            Name        = $InputObject.name
            Permissions = $InputObject.permissions
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.GroupShare')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.UserId)"
        }
        Write-Output $result
    }
}


function ConvertTo-TeamViewerManagedDevice {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id           = [guid]$InputObject.id
            Name         = $InputObject.name
            TeamViewerId = $InputObject.TeamViewerId
            IsOnline     = $InputObject.isOnline
        }

        if ($InputObject.pendingOperation) {
            $properties["PendingOperation"] = $InputObject.pendingOperation
        }

        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.ManagedDevice')
        Write-Output $result
    }
}



function ConvertTo-TeamViewerManagedGroup {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id   = [guid]$InputObject.id
            Name = $InputObject.name
        }
        if ($InputObject.policy_id) {
            $properties["PolicyId"] = $InputObject.policy_id
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.ManagedGroup')
        Write-Output $result
    }
}



function ConvertTo-TeamViewerManager {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject,

        [Parameter(Mandatory = $true, ParameterSetName = "GroupManager")]
        [guid]
        $GroupId,

        [Parameter(Mandatory = $true, ParameterSetName = "DeviceManager")]
        [guid]
        $DeviceId
    )
    process {
        $properties = @{
            Id          = [guid]$InputObject.id
            ManagerType = $InputObject.type
            Name        = $InputObject.name
            Permissions = $InputObject.permissions
        }

        switch ($InputObject.type) {
            'account' {
                $properties.AccountId = $InputObject.accountId
            }
            'company' {
                $properties.CompanyId = $InputObject.companyId
            }
        }

        switch ($PsCmdlet.ParameterSetName) {
            'GroupManager' {
                $properties.GroupId = $GroupId
            }
            'DeviceManager' {
                $properties.DeviceId = $DeviceId
            }
        }

        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Manager')
        Write-Output $result
    }
}



function ConvertTo-TeamViewerPolicy {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )

    process {
        $properties = @{
            Id       = $InputObject.policy_id
            Name     = $InputObject.name
            Settings = @(
                $InputObject.settings | ForEach-Object {
                    @{
                        Key     = $_.key
                        Value   = $_.value
                        Enforce = $_.enforce
                    }
                }
            )
        }

        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.Policy')
        Write-Output $result
    }
}



function ConvertTo-TeamViewerRestError {
    param(
        [parameter(ValueFromPipeline)]
        $InputError
    )
    Process {
        try {
            $errorObject = ($InputError | Out-String | ConvertFrom-Json)
            $result = [PSCustomObject]@{
                Message        = $errorObject.error_description
                ErrorCategory  = $errorObject.error
                ErrorCode      = $errorObject.error_code
                ErrorSignature = $errorObject.error_signature
            }
            $result | Add-Member -MemberType ScriptMethod -Name 'ToString' -Force -Value {
                Write-Output "$($this.Message) ($($this.ErrorCategory))"
            }
            $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.RestError')
            return $result
        }
        catch {
            return $InputError
        }
    }
}



function ConvertTo-TeamViewerSsoDomain {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id   = $InputObject.DomainId
            Name = $InputObject.DomainName
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.SsoDomain')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.Name)"
        }
        Write-Output $result
    }
}



function ConvertTo-TeamViewerUser {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject,

        [Parameter()]
        [ValidateSet('All', 'Minimal')]
        $PropertiesToLoad = 'All'
    )
    process {
        $properties = @{
            Id    = $InputObject.id
            Name  = $InputObject.name
            Email = $InputObject.email
        }
        if ($PropertiesToLoad -Eq 'All') {
            $properties += @{
                Permissions    = $InputObject.permissions -split ','
                Active         = $InputObject.active
                LastAccessDate = $InputObject.last_access_date | ConvertTo-DateTime
            }
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.User')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "$($this.Name) <$($this.Email)>"
        }
        Write-Output $result
    }
}


function Get-OperatingSystem {
    if ($IsLinux) {
        return 'Linux'
    }
    if ($IsMacOS) {
        return 'MacOS'
    }
    if ($IsWindows -Or $env:OS -match '^Windows') {
        return 'Windows'
    }
}



function Get-TeamViewerApiUri {
    Write-Output 'https://webapi.teamviewer.com/api/v1'
}



function Get-TeamViewerLinuxGlobalConfig {
    param(
        [Parameter()]
        [string]
        $Path = '/opt/teamviewer/config/global.conf',

        [Parameter()]
        [string]
        $Name
    )
    $config = Get-Content $Path | ForEach-Object {
        if ($_ -Match '\[(?<EntryType>\w+)\]\s+(?<EntryName>\w+)\s+=\s*(?<EntryValue>.*)$') {
            $Matches.Remove(0)
            $entry = [pscustomobject]$Matches
            switch ($entry.EntryType) {
                'strng' {
                    $entry.EntryValue = $entry.EntryValue | `
                        Select-String -Pattern '"([^\"]*)"' -AllMatches | `
                        Select-Object -ExpandProperty Matches | `
                        ForEach-Object { $_.Groups[1].Value }
                }
            }
            $entry
        }
    }

    if ($Name) {
        ($config | Where-Object { $_.EntryName -eq $Name }).EntryValue
    }
    else {
        $config
    }
}



function Get-TeamViewerRegKeyPath {
    if ([Environment]::Is64BitOperatingSystem) {
        Write-Output 'HKLM:\SOFTWARE\Wow6432Node\TeamViewer'
    }
    else {
        Write-Output 'HKLM:\SOFTWARE\TeamViewer'
    }
}



function Get-TeamViewerServiceName {
    Write-Output 'TeamViewer'
}



function Invoke-ExternalCommand {
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Command,

        [Parameter(ValueFromRemainingArguments = $true)]
        [object[]]
        $CommandArgs
    )
    & $Command @CommandArgs
}



function Invoke-TeamViewerRestMethod {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [uri]
        $Uri,

        [Microsoft.PowerShell.Commands.WebRequestMethod]
        $Method,

        [System.Collections.IDictionary]
        $Headers,

        [System.Object]
        $Body,

        [string]
        $ContentType,

        [System.Management.Automation.PSCmdlet]
        $WriteErrorTo
    )

    if (-Not $Headers) {
        $Headers = @{ }
        $PSBoundParameters.Add("Headers", $Headers) | Out-Null
    }
    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ApiToken)
    $Headers["Authorization"] = "Bearer $([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr))"
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
    $PSBoundParameters.Remove("ApiToken") | Out-Null
    $PSBoundParameters.Remove("WriteErrorTo") | Out-Null

    $currentTlsSettings = [Net.ServicePointManager]::SecurityProtocol
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # Using `Invoke-WebRequest` instead of `Invoke-RestMethod`:
    # There is a known issue for PUT and DELETE operations to hang on Windows Server 2012.
    try {
        return ((Invoke-WebRequest -UseBasicParsing @PSBoundParameters).Content | ConvertFrom-Json)
    }
    catch {
        $msg = $null
        if ($PSVersionTable.PSVersion.Major -ge 6) {
            $msg = $_.ErrorDetails.Message
        }
        elseif ($_.Exception.Response) {
            $stream = $_.Exception.Response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($stream)
            $reader.BaseStream.Position = 0
            $msg = $reader.ReadToEnd()
        }
        $err = ($msg | ConvertTo-TeamViewerRestError)
        if ($WriteErrorTo) {
            $WriteErrorTo.WriteError(($err | ConvertTo-ErrorRecord))
        }
        else {
            throw $err
        }
    }
    finally {
        [Net.ServicePointManager]::SecurityProtocol = $currentTlsSettings
    }
}



function Resolve-TeamViewerContactId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Contact
    )
    Process {
        if ($Contact.PSObject.TypeNames -contains 'TeamViewerPS.Contact') {
            return $Contact.Id
        }
        elseif ($Contact -is [string]) {
            if ($Contact -notmatch 'c[0-9]+') {
                throw "Invalid contact identifier '$Contact'. String must be a contact ID in the form 'c123456789'."
            }
            return $Contact
        }
        else {
            throw "Invalid contact identifier '$Contact'. Must be either a [TeamViewerPS.Contact] or [string]."
        }
    }
}



function Resolve-TeamViewerDeviceId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Device
    )
    Process {
        if ($Device.PSObject.TypeNames -contains 'TeamViewerPS.Device') {
            return $Device.Id
        }
        elseif ($Device -is [string]) {
            if ($Device -notmatch 'd[0-9]+') {
                throw "Invalid device identifier '$Device'. String must be a device ID in the form 'd123456789'."
            }
            return $Device
        }
        else {
            throw "Invalid device identifier '$Device'. Must be either a [TeamViewerPS.Device] or [string]."
        }
    }
}



function Resolve-TeamViewerGroupId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Group
    )
    Process {
        if ($Group.PSObject.TypeNames -contains 'TeamViewerPS.Group') {
            return $Group.Id
        }
        elseif ($Group -is [string]) {
            if ($Group -notmatch 'g[0-9]+') {
                throw "Invalid group identifier '$Group'. String must be a group ID in the form 'g123456789'."
            }
            return $Group
        }
        else {
            throw "Invalid group identifier '$Group'. Must be either a [TeamViewerPS.Group] or [string]."
        }
    }
}



function Resolve-TeamViewerLanguage {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $InputObject
    )
    Process {
        $supportedLanguages = @(
            'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hr', 'hu', 'id', 'it', 'ja',
            'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sk', 'sr', 'sv', 'th', 'tr', 'uk',
            'vi', 'zh_CN', 'zh_TW', 'auto')

        $language = $InputObject
        if ($InputObject -is [cultureinfo]) {
            $language = switch ($InputObject.Name) {
                'zh-CN' { 'zh_CN' }
                'zh-TW' { 'zh_TW' }
                default { $InputObject.TwoLetterISOLanguageName }
            }
        }

        if ($supportedLanguages -notcontains $language) {
            throw "Invalid culture '$language'. Supported languages are: $supportedLanguages"
        }

        return $language
    }
}



function Resolve-TeamViewerManagedDeviceId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $ManagedDevice
    )
    Process {
        if ($ManagedDevice.PSObject.TypeNames -contains 'TeamViewerPS.ManagedDevice') {
            return [guid]$ManagedDevice.Id
        }
        elseif ($ManagedDevice -is [string]) {
            return [guid]$ManagedDevice
        }
        elseif ($ManagedDevice -is [guid]) {
            return $ManagedDevice
        }
        else {
            throw "Invalid managed device identifier '$ManagedDevice'. Must be either a [TeamViewerPS.ManagedDevice], [guid] or [string]."
        }
    }
}



function Resolve-TeamViewerManagedGroupId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $ManagedGroup
    )
    Process {
        if ($ManagedGroup.PSObject.TypeNames -contains 'TeamViewerPS.ManagedGroup') {
            return [guid]$ManagedGroup.Id
        }
        elseif ($ManagedGroup -is [string]) {
            return [guid]$ManagedGroup
        }
        elseif ($ManagedGroup -is [guid]) {
            return $ManagedGroup
        }
        else {
            throw "Invalid managed group identifier '$ManagedGroup'. Must be either a [TeamViewerPS.ManagedGroup], [guid] or [string]."
        }
    }
}



function Resolve-TeamViewerManagerId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Manager
    )
    Process {
        if ($Manager.PSObject.TypeNames -contains 'TeamViewerPS.Manager') {
            return [guid]$Manager.Id
        }
        elseif ($Manager -is [string]) {
            return [guid]$Manager
        }
        elseif ($Manager -is [guid]) {
            return $Manager
        }
        else {
            throw "Invalid manager identifier '$Manager'. Must be either a [TeamViewerPS.Manager], [guid] or [string]."
        }
    }
}



function Resolve-TeamViewerPolicyId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Policy,

        [Parameter()]
        [switch]
        $AllowNone,

        [Parameter()]
        [switch]
        $AllowInherit
    )
    Process {
        if ($Policy.PSObject.TypeNames -contains 'TeamViewerPS.Policy') {
            return [guid]$Policy.Id
        }
        elseif ($Policy -is [string]) {
            if ($Policy -eq 'none' -And $AllowNone) {
                return 'none'
            }
            elseif ($Policy -eq 'inherit' -And $AllowInherit) {
                return 'inherit'
            }
            else {
                return [guid]$Policy
            }
        }
        elseif ($Policy -is [guid]) {
            return $Policy
        }
        else {
            throw "Invalid policy identifier '$Policy'. Must be either a [TeamViewerPS.Policy], [guid] or [string]."
        }
    }
}



function Resolve-TeamViewerSsoDomainId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $Domain
    )
    Process {
        if ($Domain.PSObject.TypeNames -contains 'TeamViewerPS.SsoDomain') {
            return [guid]$Domain.Id
        }
        elseif ($Domain -is [string]) {
            return [guid]$Domain
        }
        elseif ($Domain -is [guid]) {
            return $Domain
        }
        else {
            throw "Invalid SSO domain identifier '$Domain'. Must be either a [TeamViewerPS.SsoDomain], [guid] or [string]."
        }
    }
}



function Resolve-TeamViewerUserId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $User
    )
    Process {
        if ($User.PSObject.TypeNames -contains 'TeamViewerPS.User') {
            return $User.Id
        }
        elseif ($User -is [string]) {
            if ($User -notmatch 'u[0-9]+') {
                throw "Invalid user identifier '$User'. String must be a user ID in the form 'u123456789'."
            }
            return $User
        }
        else {
            throw "Invalid user identifier '$User'. Must be either a [TeamViewerPS.User] or [string]."
        }
    }
}



$hasTestNetConnection = [bool](Get-Command Test-NetConnection -ErrorAction SilentlyContinue)
$hasTestConnection = [bool](Get-Command Test-Connection -ErrorAction SilentlyContinue | Where-Object { $_.Version -ge 5.1 })

function Test-TcpConnection {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Hostname,

        [Parameter(Mandatory = $true)]
        [int]
        $Port
    )

    if (-Not $hasTestNetConnection -And -Not $hasTestConnection) {
        throw "No suitable cmdlet found for testing the TeamViewer network connectivity."
    }

    $oldProgressPreference = $global:ProgressPreference
    $global:ProgressPreference = 'SilentlyContinue'

    if ($hasTestNetConnection) {
        Test-NetConnection -ComputerName $Hostname -Port $Port -InformationLevel Quiet -WarningAction SilentlyContinue
    }
    elseif ($hasTestConnection) {
        Test-Connection -TargetName $Hostname -TcpPort $Port -Quiet -WarningAction SilentlyContinue
    }
    else {
        $false
    }

    $global:ProgressPreference = $oldProgressPreference
}



function Add-TeamViewerManagedDevice {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group
    )

    $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
    $groupId = $Group | Resolve-TeamViewerManagedGroupId
    $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/devices"

    $body = @{
        id = $deviceId
    }

    if ($PSCmdlet.ShouldProcess($deviceId, "Add device to managed group")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Add-TeamViewerManager {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Device_ByAccountId')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByAccountId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByAccountId')]
        [string]
        $AccountId,

        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByManagerId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByManagerId')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagerId } )]
        [Alias("ManagerId")]
        [object]
        $Manager,

        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByUserObject')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByUserObject')]
        [ValidateScript( { $_ | Resolve-TeamViewerUserId } )]
        [object]
        $User,

        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByAccountId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByManagerId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByUserObject')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByAccountId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByManagerId')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByUserObject')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]
        $Permissions
    )

    $resourceUri = $null
    switch -Wildcard ($PSCmdlet.ParameterSetName) {
        'Device*' {
            $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/devices/$deviceId/managers"
            $processMessage = "Add manager to managed device"
        }
        'Group*' {
            $groupId = $Group | Resolve-TeamViewerManagedGroupId
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/managers"
            $processMessage = "Add manager to managed group"
        }
    }

    $body = @{}
    switch -Wildcard ($PSCmdlet.ParameterSetName) {
        '*ByAccountId' {
            $body["accountId"] = $AccountId.TrimStart('u')
        }
        '*ByManagerId' {
            $body["id"] = $Manager | Resolve-TeamViewerManagerId
        }
        '*ByUserObject' {
            $body["accountId"] = $User.Id.TrimStart('u')
        }
    }

    if ($Permissions) {
        $body["permissions"] = @($Permissions)
    }
    else {
        $body["permissions"] = @()
    }

    if ($PSCmdlet.ShouldProcess($managerId, $processMessage)) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @($body)))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Add-TeamViewerSsoExclusion {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerSsoDomainId } )]
        [Alias("Domain")]
        [object]
        $DomainId,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $Email
    )
    Begin {
        $id = $DomainId | Resolve-TeamViewerSsoDomainId
        $resourceUri = "$(Get-TeamViewerApiUri)/ssoDomain/$id/exclusion"
        $emailsToAdd = @()
    }
    Process {
        if ($PSCmdlet.ShouldProcess($Email, "Add SSO exclusion")) {
            $emailsToAdd += $Email
        }
    }
    End {
        $body = @{
            emails = @($emailsToAdd)
        }

        if ($emailsToAdd.Length -gt 0) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Post `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



function Connect-TeamViewerApi {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken
    )

    if (Invoke-TeamViewerPing -ApiToken $ApiToken) {
        $global:PSDefaultParameterValues["*-Teamviewer*:ApiToken"] = $ApiToken
    }
}


function Disconnect-TeamViewerApi {
    $global:PSDefaultParameterValues.Remove("*-Teamviewer*:ApiToken")
}


function Get-TeamViewerAccount {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/account"

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop
    Write-Output ($response | ConvertTo-TeamViewerAccount)
}



function Get-TeamViewerContact {
    [CmdletBinding(DefaultParameterSetName = "FilteredList")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByContactId")]
        [ValidateScript( { $_ | Resolve-TeamViewerContactId } )]
        [Alias("ContactId")]
        [string]
        $Id,

        [Parameter(ParameterSetName = "FilteredList")]
        [Alias("PartialName")]
        [string]
        $Name,

        [Parameter(ParameterSetName = "FilteredList")]
        [ValidateSet('Online', 'Busy', 'Away', 'Offline')]
        [string]
        $FilterOnlineState,

        [Parameter(ParameterSetName = "FilteredList")]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/contacts";
    $parameters = @{ }

    switch ($PsCmdlet.ParameterSetName) {
        'ByContactId' {
            $resourceUri += "/$Id"
            $parameters = $null
        }
        'FilteredList' {
            if ($Name) {
                $parameters['name'] = $Name
            }
            if ($FilterOnlineState) {
                $parameters['online_state'] = $FilterOnlineState.ToLower()
            }
            if ($Group) {
                $groupId = $Group | Resolve-TeamViewerGroupId
                $parameters['groupid'] = $groupId
            }
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -Body $parameters `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    Write-Output ($response.contacts | ConvertTo-TeamViewerContact)
}



function Get-TeamViewerDevice {
    [CmdletBinding(DefaultParameterSetName = "FilteredList")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByDeviceId")]
        [ValidateScript( { $_ | Resolve-TeamViewerDeviceId } )]
        [Alias("DeviceId")]
        [string]
        $Id,

        [Parameter(ParameterSetName = "FilteredList")]
        [int]
        $TeamViewerId,

        [Parameter(ParameterSetName = "FilteredList")]
        [ValidateSet('Online', 'Busy', 'Away', 'Offline')]
        [string]
        $FilterOnlineState,

        [Parameter(ParameterSetName = "FilteredList")]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/devices";
    $parameters = @{ }

    switch ($PsCmdlet.ParameterSetName) {
        'ByDeviceId' {
            $resourceUri += "/$Id"
            $parameters = $null
        }
        'FilteredList' {
            if ($TeamViewerId) {
                $parameters['remotecontrol_id'] = "r$TeamViewerId"
            }
            if ($FilterOnlineState) {
                $parameters['online_state'] = $FilterOnlineState.ToLower()
            }
            if ($Group) {
                $groupId = $Group | Resolve-TeamViewerGroupId
                $parameters['groupid'] = $groupId
            }
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -Body $parameters `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    Write-Output ($response.devices | ConvertTo-TeamViewerDevice)
}



function Get-TeamViewerGroup {
    [CmdletBinding(DefaultParameterSetName = "FilteredList")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByGroupId")]
        [Alias("GroupId")]
        [string]
        $Id,

        [Parameter(ParameterSetName = "FilteredList")]
        [Alias("PartialName")]
        [string]
        $Name,

        [Parameter(ParameterSetName = "FilteredList")]
        [ValidateSet('OnlyShared', 'OnlyNotShared')]
        [string]
        $FilterShared
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/groups";
    $parameters = @{ }

    switch ($PsCmdlet.ParameterSetName) {
        'ByGroupId' {
            $resourceUri += "/$Id"
            $parameters = $null
        }
        'FilteredList' {
            if ($Name) {
                $parameters['name'] = $Name
            }
            switch ($FilterShared) {
                'OnlyShared' { $parameters['shared'] = $true }
                'OnlyNotShared' { $parameters['shared'] = $false }
            }
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -Body $parameters `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    if ($PsCmdlet.ParameterSetName -Eq 'ByGroupId') {
        Write-Output ($response | ConvertTo-TeamViewerGroup)
    }
    else {
        Write-Output ($response.groups | ConvertTo-TeamViewerGroup)
    }
}



function Get-TeamViewerId {
    if (Test-TeamViewerInstallation) {
        switch (Get-OperatingSystem) {
            'Windows' {
                Write-Output (Get-ItemPropertyValue -Path (Get-TeamViewerRegKeyPath) -Name 'ClientID')
            }
            'Linux' {
                Write-Output (Get-TeamViewerLinuxGlobalConfig -Name 'ClientID')
            }
        }
    }
}



function Get-TeamViewerManagedDevice {
    [CmdletBinding(DefaultParameterSetName = "List")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByDeviceId")]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [guid]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = "ListGroup")]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(ParameterSetName = "ListGroup")]
        [switch]
        $Pending
    )

    # default is 'List':
    $resourceUri = "$(Get-TeamViewerApiUri)/managed/devices";
    $parameters = @{ }
    $isListOperation = $true

    switch ($PsCmdlet.ParameterSetName) {
        'ByDeviceId' {
            $resourceUri += "/$Id"
            $parameters = $null
            $isListOperation = $false
        }
        'ListGroup' {
            $groupId = $Group | Resolve-TeamViewerManagedGroupId
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/$(if ($Pending) { "pending-" })devices"
        }
    }

    do {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Get `
            -Body $parameters `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        if ($PsCmdlet.ParameterSetName -Eq 'ByDeviceId') {
            Write-Output ($response | ConvertTo-TeamViewerManagedDevice)
        }
        else {
            $parameters.paginationToken = $response.nextPaginationToken
            Write-Output ($response.resources | ConvertTo-TeamViewerManagedDevice)
        }
    } while ($isListOperation -And $parameters.paginationToken)
}



function Get-TeamViewerManagedGroup {
    [CmdletBinding(DefaultParameterSetName = "List")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByGroupId")]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } ) ]
        [Alias("GroupId")]
        [guid]
        $Id
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups";
    $parameters = @{ }

    switch ($PsCmdlet.ParameterSetName) {
        'ByGroupId' {
            $resourceUri += "/$Id"
            $parameters = $null
        }
    }

    do {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Get `
            -Body $parameters `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        if ($PsCmdlet.ParameterSetName -Eq 'ByGroupId') {
            Write-Output ($response | ConvertTo-TeamViewerManagedGroup)
        }
        else {
            $parameters.paginationToken = $response.nextPaginationToken
            Write-Output ($response.resources | ConvertTo-TeamViewerManagedGroup)
        }
    } while ($PsCmdlet.ParameterSetName -Eq 'List' -And $parameters.paginationToken)
}



function Get-TeamViewerManager {
    [CmdletBinding(DefaultParameterSetName = "ByDeviceId")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ParameterSetName = "ByDeviceId")]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter(Mandatory = $true, ParameterSetName = "ByGroupId")]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group
    )

    $resourceUri = $null
    switch ($PsCmdlet.ParameterSetName) {
        'ByDeviceId' {
            $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/devices/$deviceId/managers"
        }
        'ByGroupId' {
            $groupId = $Group | Resolve-TeamViewerManagedGroupId
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/managers"
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    switch ($PsCmdlet.ParameterSetName) {
        'ByDeviceId' {
            Write-Output ($response.resources | ConvertTo-TeamViewerManager -DeviceId $deviceId)
        }
        'ByGroupId' {
            Write-Output ($response.resources | ConvertTo-TeamViewerManager -GroupId $groupId)
        }
    }
}



function Get-TeamViewerPolicy {
    [CmdletBinding(DefaultParameterSetName = "FilteredList")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByPolicyId")]
        [Alias("PolicyId")]
        [guid]
        $Id
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/teamviewerpolicies";
    $parameters = @{ }

    switch ($PsCmdlet.ParameterSetName) {
        'ByPolicyId' {
            $resourceUri += "/$Id"
            $parameters = $null
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -Body $parameters `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    Write-Output ($response.policies | ConvertTo-TeamViewerPolicy)
}




function Get-TeamViewerService {
    switch (Get-OperatingSystem) {
        'Windows' {
            Get-Service -Name (Get-TeamViewerServiceName)
        }
        'Linux' {
            Invoke-ExternalCommand /opt/teamviewer/tv_bin/script/teamviewer daemon status
        }
    }
}



function Get-TeamViewerSsoDomain {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/ssoDomain";
    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop
    Write-Output ($response.domains | ConvertTo-TeamViewerSsoDomain)
}



function Get-TeamViewerSsoExclusion {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerSsoDomainId } )]
        [Alias("Domain")]
        [object]
        $DomainId
    )

    $id = $DomainId | Resolve-TeamViewerSsoDomainId
    $resourceUri = "$(Get-TeamViewerApiUri)/ssoDomain/$id/exclusion";
    $parameters = @{ }
    do {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Get `
            -Body $parameters `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        Write-Output $response.emails
        $parameters.ct = $response.continuation_token
    } while ($parameters.ct)
}



function Get-TeamViewerUser {
    [CmdletBinding(DefaultParameterSetName = "FilteredList")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = "ByUserId")]
        [ValidateScript( { $_ | Resolve-TeamViewerUserId } )]
        [Alias("UserId")]
        [string]
        $Id,

        [Parameter(ParameterSetName = "FilteredList")]
        [Alias("PartialName")]
        [string]
        $Name,

        [Parameter(ParameterSetName = "FilteredList")]
        [string[]]
        $Email,

        [Parameter(ParameterSetName = "FilteredList")]
        [string[]]
        $Permissions,

        [Parameter()]
        [ValidateSet("All", "Minimal")]
        $PropertiesToLoad = "All"
    )

    $parameters = @{ }
    switch ($PropertiesToLoad) {
        "All" { $parameters.full_list = $true }
        "Minimal" { }
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/users"

    switch ($PsCmdlet.ParameterSetName) {
        "ByUserId" {
            $resourceUri += "/$Id"
            $parameters = $null
        }
        "FilteredList" {
            if ($Name) {
                $parameters['name'] = $Name
            }
            if ($Email) {
                $parameters['email'] = ($Email -join ',')
            }
            if ($Permissions) {
                $parameters['permissions'] = ($Permissions -join ',')
            }
        }
    }

    $response = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -Body $parameters `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop

    if ($PsCmdlet.ParameterSetName -Eq "ByUserId") {
        Write-Output ($response | ConvertTo-TeamViewerUser -PropertiesToLoad $PropertiesToLoad)
    }
    else {
        Write-Output ($response.users | ConvertTo-TeamViewerUser -PropertiesToLoad $PropertiesToLoad)
    }
}



function Get-TeamViewerVersion {
    if (Test-TeamViewerInstallation) {
        switch (Get-OperatingSystem) {
            'Windows' {
                Write-Output (Get-ItemPropertyValue -Path (Get-TeamViewerRegKeyPath) -Name 'Version')
            }
            'Linux' {
                Write-Output (Get-TeamViewerLinuxGlobalConfig -Name 'Version')
            }
        }
    }
}



function Invoke-TeamViewerPackageDownload {
    Param(
        [Parameter()]
        [ValidateSet('Full', 'Host', 'Portable', 'QuickJoin', 'QuickSupport')]
        [ValidateScript( {
                if (($_ -ne 'Full') -And ((Get-OperatingSystem) -ne 'Windows')) {
                    $PSCmdlet.ThrowTerminatingError(
                        ("PackageType parameter is only supported on Windows platforms" | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }
                $true
            })]
        [string]
        $PackageType = 'Full',

        [Parameter()]
        [ValidateScript( {
                if ((Get-OperatingSystem) -ne 'Windows') {
                    $PSCmdlet.ThrowTerminatingError(
                        ("MajorVersion parameter is only supported on Windows platforms" | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }
                if ($_ -lt 14) {
                    $PSCmdlet.ThrowTerminatingError(
                        ("Unsupported TeamViewer version $_" | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }
                $true
            } )]
        [int]
        $MajorVersion,

        [Parameter()]
        [string]
        $TargetDirectory = (Get-Location).Path,

        [Parameter()]
        [switch]
        $Force
    )

    $additionalPath = ''
    switch (Get-OperatingSystem) {
        'Windows' {
            $filename = switch ($PackageType) {
                'Full' { 'TeamViewer_Setup.exe' }
                'Host' { 'TeamViewer_Host_Setup.exe' }
                'Portable' { 'TeamViewerPortable.zip' }
                'QuickJoin' { 'TeamViewerQJ.exe' }
                'QuickSupport' { 'TeamViewerQS.exe' }
            }
            if ($MajorVersion) {
                $additionalPath = "/version_$($MajorVersion)x"
            }
        }
        'Linux' {
            $releaseInfo = (Get-Content /etc/*-release)
            $filename = switch -Regex ($releaseInfo) {
                'debian|ubuntu' {
                    $platform = if ([Environment]::Is64BitOperatingSystem) { 'amd64' } else { 'i386' }
                    "teamviewer_$platform.deb"
                }
                'centos|rhel|fedora' {
                    $platform = if ([Environment]::Is64BitOperatingSystem) { 'x86_64' } else { 'i686' }
                    "teamviewer.$platform.rpm"
                }
                'suse|opensuse' {
                    $platform = if ([Environment]::Is64BitOperatingSystem) { 'x86_64' } else { 'i686' }
                    "teamviewer-suse.$platform.rpm"
                }
            }
            $filename = $filename | Select-Object -First 1
            $additionalPath = '/linux'
        }
    }

    $downloadUrl = "https://download.teamviewer.com/download$additionalPath/$filename"
    $targetFile = Join-Path $TargetDirectory $filename

    if ((Test-Path $targetFile) -And -Not $Force -And `
            -Not $PSCmdlet.ShouldContinue("File $targetFile already exists. Override?", "Override existing file?")) {
        return
    }

    Write-Verbose "Downloading $downloadUrl to $targetFile"
    $client = New-Object System.Net.WebClient
    $client.DownloadFile($downloadUrl, $targetFile)
    Write-Output $targetFile
}



function Invoke-TeamViewerPing {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/ping"
    $result = Invoke-TeamViewerRestMethod `
        -ApiToken $ApiToken `
        -Uri $resourceUri `
        -Method Get `
        -WriteErrorTo $PSCmdlet `
        -ErrorAction Stop
    Write-Output $result.token_valid
}



function New-TeamViewerContact {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [Alias('EmailAddress')]
        [string]
        $Email,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter()]
        [switch]
        $Invite
    )

    $body = @{
        email   = $Email
        groupid = $Group | Resolve-TeamViewerGroupId
    }
    if ($Invite) {
        $body['invite'] = $true
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/contacts"
    if ($PSCmdlet.ShouldProcess($Email, "Create contact")) {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        $result = ($response | ConvertTo-TeamViewerContact)
        Write-Output $result
    }
}



function New-TeamViewerDevice {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [int]
        $TeamViewerId,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter()]
        [Alias("Alias")]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [securestring]
        $Password
    )

    $body = @{
        remotecontrol_id = "r$TeamViewerId"
        groupid          = $Group | Resolve-TeamViewerGroupId
    }

    if ($Name) {
        $body['alias'] = $Name
    }
    if ($Description) {
        $body['description'] = $Description
    }
    if ($Password) {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
        $body['password'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/devices"
    if ($PSCmdlet.ShouldProcess($TeamViewerId, "Create device entry")) {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        $result = ($response | ConvertTo-TeamViewerDevice)
        Write-Output $result
    }
}



function New-TeamViewerGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [ValidateScript( { $_ | Resolve-TeamViewerPolicyId } )]
        [Alias("PolicyId")]
        [object]
        $Policy
    )

    $body = @{ name = $Name }
    if ($Policy) {
        $body["policy_id"] = $Policy | Resolve-TeamViewerPolicyId
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/groups"
    if ($PSCmdlet.ShouldProcess($Name, "Create group")) {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop
        Write-Output ($response | ConvertTo-TeamViewerGroup)
    }
}



function New-TeamViewerManagedGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $body = @{ name = $Name }
    $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups"
    if ($PSCmdlet.ShouldProcess($Name, "Create managed group")) {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop
        Write-Output ($response | ConvertTo-TeamViewerManagedGroup)
    }
}



function New-TeamViewerPolicy {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [AllowEmptyCollection()]
        [object[]]
        $Settings,

        [Parameter()]
        [switch]
        $DefaultPolicy = $False
    )

    $body = @{
        name     = $Name
        default  = [boolean]$DefaultPolicy
        settings = @()
    }

    if ($Settings) {
        $body.settings = @($Settings)
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/teamviewerpolicies"
    if ($PSCmdlet.ShouldProcess($Name, "Create policy")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function New-TeamViewerUser {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "WithPassword")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [Alias('EmailAddress')]
        [string]
        $Email,

        [Parameter(Mandatory = $true)]
        [Alias('DisplayName')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = "WithPassword")]
        [securestring]
        $Password,

        [Parameter(ParameterSetName = "WithoutPassword")]
        [Alias('NoPassword')]
        [switch]
        $WithoutPassword,

        [Parameter()]
        [securestring]
        $SsoCustomerIdentifier,

        [Parameter()]
        [array]
        $Permissions,

        [Parameter()]
        [ValidateScript( { $_ | Resolve-TeamViewerLanguage } )]
        [cultureinfo]
        $Culture
    )

    if (-Not $Culture) {
        try { $Culture = Get-Culture }
        catch { $Culture = 'en' }
    }

    $body = @{
        email       = $Email
        name        = $Name
        language    = $Culture | Resolve-TeamViewerLanguage
        permissions = $Permissions -join ','
    }

    if ($Password -And -Not $WithoutPassword) {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
        $body['password'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
    }

    if ($SsoCustomerIdentifier) {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SsoCustomerIdentifier)
        $body['sso_customer_id'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/users"
    if ($PSCmdlet.ShouldProcess("$Name <$Email>", "Create user")) {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop

        $result = ($response | ConvertTo-TeamViewerUser)
        $result.Email = $Email
        Write-Output $result
    }
}



function Publish-TeamViewerGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserId } )]
        [Alias("UserId")]
        [object[]]
        $User,

        [Parameter()]
        [ValidateSet("read", "readwrite")]
        $Permissions = "read"
    )

    # Warning suppresion doesn't seem to work.
    # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
    $null = $Permissions

    $groupId = $Group | Resolve-TeamViewerGroupId
    $userIds = $User | Resolve-TeamViewerUserId
    $resourceUri = "$(Get-TeamViewerApiUri)/groups/$groupId/share_group"
    $body = @{
        users = @($userIds | ForEach-Object { @{
                    userid      = $_
                    permissions = $Permissions
                } })
    }

    if ($PSCmdlet.ShouldProcess($userids, "Add group share")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Remove-TeamViewerContact {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerContactId } )]
        [Alias("ContactId")]
        [Alias("Id")]
        [object]
        $Contact
    )
    Process {
        $contactId = $Contact | Resolve-TeamViewerContactId
        $resourceUri = "$(Get-TeamViewerApiUri)/contacts/$contactId"
        if ($PSCmdlet.ShouldProcess($contactId, "Remove contact")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



function Remove-TeamViewerDevice {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerDeviceId } )]
        [Alias("DeviceId")]
        [Alias("Id")]
        [object]
        $Device
    )
    Process {
        $deviceId = $Device | Resolve-TeamViewerDeviceId
        $resourceUri = "$(Get-TeamViewerApiUri)/devices/$deviceId"
        if ($PSCmdlet.ShouldProcess($deviceId, "Remove device entry")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



function Remove-TeamViewerGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [Alias("Id")]
        [object]
        $Group
    )
    Process {
        $groupId = $Group | Resolve-TeamViewerGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/groups/$groupId"

        if ($PSCmdlet.ShouldProcess($groupId, "Remove group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Remove-TeamViewerManagedDevice {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter()]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group
    )
    Process {
        $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
        $groupId = $Group | Resolve-TeamViewerManagedGroupId

        $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/devices/$deviceId"

        if ($PSCmdlet.ShouldProcess($deviceId, "Remove device from managed group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Remove-TeamViewerManagedGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId } )]
        [Alias("GroupId")]
        [Alias("Id")]
        [object]
        $Group
    )
    Process {
        $groupId = $Group | Resolve-TeamViewerManagedGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId"

        if ($PSCmdlet.ShouldProcess($groupId, "Remove managed group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Remove-TeamViewerManager {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "ByDeviceId")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( {
                if (($_.PSObject.TypeNames -contains 'TeamViewerPS.Manager') -And -Not $_.GroupId -And -Not $_.DeviceId) {
                    $PSCmdlet.ThrowTerminatingError(
                        ("Invalid manager object. Manager must be a group or device manager." | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }
                $_ | Resolve-TeamViewerManagerId
            })]
        [Alias("ManagerId")]
        [Alias("Id")]
        [object]
        $Manager,

        [Parameter(ParameterSetName = 'ByDeviceId')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter(ParameterSetName = 'ByGroupId')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId })]
        [Alias("GroupId")]
        [object]
        $Group
    )
    Process {
        $deviceId = $null
        $groupId = $null
        if ($Manager.PSObject.TypeNames -contains 'TeamViewerPS.Manager') {
            if ($Device -Or $Group) {
                $PSCmdlet.ThrowTerminatingError(
                    ("Device or Group parameter must not be specified if a [TeamViewerPS.Manager] object is given." | `
                            ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
            }
            if ($Manager.DeviceId) {
                $deviceId = $Manager.DeviceId
            }
            elseif ($Manager.GroupId) {
                $groupId = $Manager.GroupId
            }
        }
        elseif ($Device) {
            $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
        }
        elseif ($Group) {
            $groupId = $Group | Resolve-TeamViewerManagedGroupId
        }
        else {
            $PSCmdlet.ThrowTerminatingError(
                ("Device or Group parameter must be specified if no [TeamViewerPS.Manager] object is given." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }

        $managerId = $Manager | Resolve-TeamViewerManagerId
        if ($deviceId) {
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/devices/$deviceId/managers/$managerId"
            $processMessage = "Remove manager from managed device"
        }
        elseif ($groupId) {
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/managers/$managerId"
            $processMessage = "Remove manager from managed group"
        }

        if ($PSCmdlet.ShouldProcess($managerId, $processMessage)) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Remove-TeamViewerPolicy {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerPolicyId } )]
        [Alias('PolicyId')]
        [object]
        $Policy
    )
    Process {
        $policyId = $Policy | Resolve-TeamViewerPolicyId
        $resourceUri = "$(Get-TeamViewerApiUri)/teamviewerpolicies/$policyId"

        if ($PSCmdlet.ShouldProcess($policyId, "Delete policy")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Remove-TeamViewerSsoExclusion {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerSsoDomainId } )]
        [Alias("Domain")]
        [object]
        $DomainId,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $Email
    )
    Begin {
        $id = $DomainId | Resolve-TeamViewerSsoDomainId
        $resourceUri = "$(Get-TeamViewerApiUri)/ssoDomain/$id/exclusion"
        $emailsToRemove = @()
    }
    Process {
        if ($PSCmdlet.ShouldProcess($Email, "Remove SSO exclusion")) {
            $emailsToRemove += $Email
        }
    }
    End {
        $body = @{
            emails = @($emailsToRemove)
        }

        if ($emailsToRemove.Length -gt 0) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



function Restart-TeamViewerService {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()

    if ($PSCmdlet.ShouldProcess("TeamViewer service")) {
        switch (Get-OperatingSystem) {
            'Windows' {
                Restart-Service -Name (Get-TeamViewerServiceName)
            }
            'Linux' {
                Invoke-ExternalCommand /opt/teamviewer/tv_bin/script/teamviewer daemon restart
            }
        }
    }
}



function Set-TeamViewerAccount {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(ParameterSetName = 'ByParameters')]
        [Alias('DisplayName')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByParameters')]
        [Alias('EmailAddress')]
        [string]
        $Email,

        [Parameter(ParameterSetName = 'ByParameters')]
        [securestring]
        $Password,

        [Parameter(ParameterSetName = 'ByParameters')]
        [securestring]
        $OldPassword,

        [Parameter(ParameterSetName = 'ByParameters')]
        [ValidateScript( { $_ | Resolve-TeamViewerLanguage } )]
        [object]
        $EmailLanguage,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByProperties')]
        [hashtable]
        $Property
    )

    # Warning suppresion doesn't seem to work.
    # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
    $null = $Property

    $body = @{}
    switch ($PSCmdlet.ParameterSetName) {
        'ByParameters' {
            if ($Name) {
                $body['name'] = $Name
            }
            if ($Email) {
                $body['email'] = $Email
            }
            if ($Password) {
                if (-Not $OldPassword) {
                    $PSCmdlet.ThrowTerminatingError(
                        ("Old password required when attempting to change account password." | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }

                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
                $body['password'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null

                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($OldPassword)
                $body['oldpassword'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
            }
            if ($EmailLanguage) {
                $body['email_language'] = $EmailLanguage | Resolve-TeamViewerLanguage
            }
        }
        'ByProperties' {
            @('name', 'email', 'password', 'oldpassword', 'email_language') | `
                Where-Object { $Property[$_] } | `
                ForEach-Object { $body[$_] = $Property[$_] }
        }
    }

    if ($body.Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError(
            ("The given input does not change the account." | `
                    ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
    }

    $resourceUri = "$(Get-TeamViewerApiUri)/account"

    if ($PSCmdlet.ShouldProcess("TeamViewer account")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Put `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Set-TeamViewerDevice {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerDeviceId } )]
        [Alias("DeviceId")]
        [Alias("Id")]
        [object]
        $Device,

        [Parameter(ParameterSetName = "ChangeGroup")]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(ParameterSetName = "ChangePolicy")]
        [ValidateScript( { $_ | Resolve-TeamViewerPolicyId -AllowInherit -AllowNone } )]
        [Alias("PolicyId")]
        [object]
        $Policy,

        [Parameter()]
        [Alias("Alias")]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [securestring]
        $Password
    )
    Begin {
        $body = @{}

        if ($Name) {
            $body['alias'] = $Name
        }
        if ($Description) {
            $body['description'] = $Description
        }
        if ($Password) {
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
            $body['password'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
            [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
        }
        if ($Group) {
            $body['groupid'] = $Group | Resolve-TeamViewerGroupId
        }
        if ($Policy) {
            $body['policy_id'] = $Policy | Resolve-TeamViewerPolicyId -AllowNone -AllowInherit
        }

        if ($body.Count -eq 0) {
            $PSCmdlet.ThrowTerminatingError(
                ("The given input does not change the device." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }
    }
    Process {
        $deviceId = $Device | Resolve-TeamViewerDeviceId
        $resourceUri = "$(Get-TeamViewerApiUri)/devices/$deviceId"

        if ($PSCmdlet.ShouldProcess($Device.ToString(), "Change device entry")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Put `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



function Set-TeamViewerGroup {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [Alias("Id")]
        [object]
        $Group,

        [Parameter(ParameterSetName = 'ByParameters')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByParameters')]
        [ValidateScript( { $_ | Resolve-TeamViewerPolicyId } )]
        [Alias("PolicyId")]
        [object]
        $Policy,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByProperties')]
        [hashtable]
        $Property
    )
    Begin {
        # Warning suppresion doesn't seem to work.
        # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
        $null = $Property

        $body = @{}
        switch ($PSCmdlet.ParameterSetName) {
            'ByParameters' {
                $body['name'] = $Name
                if ($Policy) {
                    $body['policy_id'] = $Policy | Resolve-TeamViewerPolicyId
                }
            }
            'ByProperties' {
                @('name', 'policy_id') | `
                    Where-Object { $Property[$_] } | `
                    ForEach-Object { $body[$_] = $Property[$_] }
            }
        }

        if ($body.Count -eq 0) {
            $PSCmdlet.ThrowTerminatingError(
                ("The given input does not change the group." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }
    }
    Process {
        $groupId = $Group | Resolve-TeamViewerGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/groups/$groupId"

        if ($PSCmdlet.ShouldProcess($groupId, "Update group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Put `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Set-TeamViewerManagedGroup {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId })]
        [Alias("GroupId")]
        [Alias("Id")]
        [object]
        $Group,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByParameters')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByProperties')]
        [hashtable]
        $Property
    )
    Begin {
        # Warning suppresion doesn't seem to work.
        # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
        $null = $Property

        $body = @{}
        switch ($PSCmdlet.ParameterSetName) {
            'ByParameters' {
                $body['name'] = $Name
            }
            'ByProperties' {
                @('name') | `
                    Where-Object { $Property[$_] } | `
                    ForEach-Object { $body[$_] = $Property[$_] }
            }
        }

        if ($body.Count -eq 0) {
            $PSCmdlet.ThrowTerminatingError(
                ("The given input does not change the managed group." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }
    }
    Process {
        $groupId = $Group | Resolve-TeamViewerManagedGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId"

        if ($PSCmdlet.ShouldProcess($groupId, "Update managed group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Put `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Set-TeamViewerManager {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Device_ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript( {
                if (($_.PSObject.TypeNames -contains 'TeamViewerPS.Manager') -And -Not $_.GroupId -And -Not $_.DeviceId) {
                    $PSCmdlet.ThrowTerminatingError(
                        ("Invalid manager object. Manager must be a group or device manager." | `
                                ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
                }
                $_ | Resolve-TeamViewerManagerId
            })]
        [Alias("ManagerId")]
        [Alias("Id")]
        [object]
        $Manager,

        [Parameter(ParameterSetName = 'Device_ByParameters')]
        [Parameter(ParameterSetName = 'Device_ByProperties')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedDeviceId } )]
        [Alias("DeviceId")]
        [object]
        $Device,

        [Parameter(ParameterSetName = 'Group_ByParameters')]
        [Parameter(ParameterSetName = 'Group_ByProperties')]
        [ValidateScript( { $_ | Resolve-TeamViewerManagedGroupId })]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(ParameterSetName = 'Device_ByParameters')]
        [Parameter(ParameterSetName = 'Group_ByParameters')]
        [AllowEmptyCollection()]
        [string[]]
        $Permissions,

        [Parameter(Mandatory = $true, ParameterSetName = 'Device_ByProperties')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Group_ByProperties')]
        [hashtable]
        $Property
    )
    Begin {
        # Warning suppresion doesn't seem to work.
        # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
        $null = $Property

        $body = @{}
        switch -Wildcard ($PSCmdlet.ParameterSetName) {
            '*ByParameters' {
                $body['permissions'] = @($Permissions)
            }
            '*ByProperties' {
                @('permissions') | `
                    Where-Object { $Property[$_] } | `
                    ForEach-Object { $body[$_] = $Property[$_] }
            }
        }

        if ($body.Count -eq 0) {
            $PSCmdlet.ThrowTerminatingError(
                ("The given input does not change the manager." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }
    }
    Process {
        $deviceId = $null
        $groupId = $null
        if ($Manager.PSObject.TypeNames -contains 'TeamViewerPS.Manager') {
            if ($Device -Or $Group) {
                $PSCmdlet.ThrowTerminatingError(
                    ("Device or Group parameter must not be specified if a [TeamViewerPS.Manager] object is given." | `
                            ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
            }
            if ($Manager.DeviceId) {
                $deviceId = $Manager.DeviceId
            }
            elseif ($Manager.GroupId) {
                $groupId = $Manager.GroupId
            }
        }
        elseif ($Device) {
            $deviceId = $Device | Resolve-TeamViewerManagedDeviceId
        }
        elseif ($Group) {
            $groupId = $Group | Resolve-TeamViewerManagedGroupId
        }
        else {
            $PSCmdlet.ThrowTerminatingError(
                ("Device or Group parameter must be specified if no [TeamViewerPS.Manager] object is given." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }

        $managerId = $Manager | Resolve-TeamViewerManagerId
        if ($deviceId) {
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/devices/$deviceId/managers/$managerId"
            $processMessage = "Update managed device manager"
        }
        elseif ($groupId) {
            $resourceUri = "$(Get-TeamViewerApiUri)/managed/groups/$groupId/managers/$managerId"
            $processMessage = "Update managed group manager"
        }

        if ($PSCmdlet.ShouldProcess($managerId, $processMessage)) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Put `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet | `
                Out-Null
        }
    }
}



function Set-TeamViewerPolicy {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerPolicyId } )]
        [Alias('PolicyId')]
        [object]
        $Policy,

        [Parameter(ParameterSetName = 'ByParameters')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByParameters')]
        [object[]]
        $Settings,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByProperties')]
        [hashtable]
        $Property
    )
    # Warning suppresion doesn't seem to work.
    # See https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
    $null = $Property

    $body = @{}
    switch ($PSCmdlet.ParameterSetName) {
        'ByParameters' {
            if ($Name) {
                $body['name'] = $Name
            }
            if ($Settings) {
                $body['settings'] = $Settings
            }
        }
        'ByProperties' {
            @('name', 'settings') | `
                Where-Object { $Property[$_] } | `
                ForEach-Object { $body[$_] = $Property[$_] }
        }
    }

    if ($body.Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError(
            ("The given input does not change the policy." | `
                    ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
    }

    $policyId = $Policy | Resolve-TeamViewerPolicyId
    $resourceUri = "$(Get-TeamViewerApiUri)/teamviewerpolicies/$policyId"

    if ($PSCmdlet.ShouldProcess($policyId, "Update policy")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Put `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Set-TeamViewerUser {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByParameters')]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserId } )]
        [Alias("UserId")]
        [Alias("Id")]
        [object]
        $User,

        [Parameter(ParameterSetName = 'ByParameters')]
        [boolean]
        $Active,

        [Parameter(ParameterSetName = 'ByParameters')]
        [Alias('EmailAddress')]
        [string]
        $Email,

        [Parameter(ParameterSetName = 'ByParameters')]
        [Alias('DisplayName')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByParameters')]
        [securestring]
        $Password,

        [Parameter(ParameterSetName = 'ByParameters')]
        [securestring]
        $SsoCustomerIdentifier,

        [Parameter(ParameterSetName = 'ByParameters')]
        [array]
        $Permissions,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByProperties')]
        [hashtable]
        $Property
    )

    $body = @{}
    switch ($PSCmdlet.ParameterSetName) {
        'ByParameters' {
            if ($PSBoundParameters.ContainsKey('Active')) {
                $body['active'] = $Active
            }
            if ($Email) {
                $body['email'] = $Email
            }
            if ($Name) {
                $body['name'] = $Name
            }
            if ($Password) {
                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
                $body['password'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
            }
            if ($SsoCustomerIdentifier) {
                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SsoCustomerIdentifier)
                $body['sso_customer_id'] = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) | Out-Null
            }
            if ($Permissions) {
                $body['permissions'] = $Permissions -join ','
            }
        }
        'ByProperties' {
            @('active', 'email', 'name', 'password', 'sso_customer_id', 'permissions') | `
                Where-Object { $Property[$_] } | `
                ForEach-Object { $body[$_] = $Property[$_] }
        }
    }

    if ($body.Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError(
            ("The given input does not change the user." | `
                    ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
    }

    $userId = Resolve-TeamViewerUserId -User $User
    $resourceUri = "$(Get-TeamViewerApiUri)/users/$userId"

    if ($PSCmdlet.ShouldProcess($userId, "Update user profile")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Put `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}



function Start-TeamViewerService {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()

    if ($PSCmdlet.ShouldProcess("TeamViewer service")) {
        switch (Get-OperatingSystem) {
            'Windows' {
                Start-Service -Name (Get-TeamViewerServiceName)
            }
            'Linux' {
                Invoke-ExternalCommand /opt/teamviewer/tv_bin/script/teamviewer daemon start
            }
        }
    }
}



function Stop-TeamViewerService {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()

    if ($PSCmdlet.ShouldProcess("TeamViewer service")) {
        switch (Get-OperatingSystem) {
            'Windows' {
                Stop-Service -Name (Get-TeamViewerServiceName)
            }
            'Linux' {
                Invoke-ExternalCommand /opt/teamviewer/tv_bin/script/teamviewer daemon stop
            }
        }
    }
}



function Test-TeamViewerConnectivity {
    param(
        [Parameter()]
        [switch]
        $Quiet
    )

    $endpoints = @(
        @{ Hostname = 'webapi.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'login.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'meeting.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'sso.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'download.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'configdl.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'get.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'go.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'client.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'feedbackservice.teamviewer.com'; TcpPort = 443; }
        @{ Hostname = 'remotescriptingstorage.blob.core.windows.net'; TcpPort = 443; }
        @{ Hostname = 'chatlivestorage.blob.core.windows.net'; TcpPort = 443; }
    )
    1..16 | ForEach-Object {
        $endpoints += @{ Hostname = "router$_.teamviewer.com"; TcpPort = (5938, 443, 80) }
    }

    $results = $endpoints | ForEach-Object {
        $endpoint = $_
        $portSucceeded = $endpoint.TcpPort | Where-Object {
            Write-Verbose "Checking endpoint $($endpoint.Hostname) on port $_"
            Test-TcpConnection -Hostname $endpoint.Hostname -Port $_
        } | Select-Object -First 1
        $endpoint.Succeeded = [bool]$portSucceeded
        $endpoint.TcpPort = if ($endpoint.Succeeded) { $portSucceeded } else { $endpoint.TcpPort }
        return $endpoint
    }

    if ($Quiet) {
        ![bool]($results | Where-Object { -Not $_.Succeeded })
    }
    else {
        $results | `
            ForEach-Object { New-Object PSObject -Property $_ } | `
            Select-Object -Property Hostname, TcpPort, Succeeded | `
            Sort-Object Hostname
    }
}



function Test-TeamViewerInstallation {
    switch (Get-OperatingSystem) {
        'Windows' {
            Write-Output (
                (Test-Path (Get-TeamViewerRegKeyPath)) -And `
                (Test-Path "$(Get-ItemPropertyValue -Path (Get-TeamViewerRegKeyPath) -Name 'InstallationDirectory')/TeamViewer.exe")
            )
        }
        'Linux' {
            Write-Output (
                (Test-Path '/opt/teamviewer/tv_bin/TeamViewer')
            )
        }
        default { $false }
    }
}



function Unpublish-TeamViewerGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerGroupId } )]
        [Alias("GroupId")]
        [object]
        $Group,

        [Parameter(Mandatory = $true)]
        [Alias("UserId")]
        [object[]]
        $User
    )

    $groupId = $Group | Resolve-TeamViewerGroupId
    $userIds = $User | Resolve-TeamViewerUserId
    $resourceUri = "$(Get-TeamViewerApiUri)/groups/$groupId/unshare_group"
    $body = @{users = @($userIds) }

    if ($PSCmdlet.ShouldProcess($userids, "Remove group share")) {
        Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet | `
            Out-Null
    }
}