TeamViewerPS.psm1

enum TeamViewerConnectionReportSessionType {
    RemoteConnection = 1
    RemoteSupportActive = 2
    RemoteSupportActiveSdk = 3
}



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-TeamViewerAuditEvent {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Name         = $InputObject.EventName
            Type         = $InputObject.EventType
            Timestamp    = $InputObject.Timestamp | ConvertTo-DateTime
            Author       = $InputObject.Author
            AffectedItem = $InputObject.AffectedItem
            EventDetails = $InputObject.EventDetails
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.AuditEvent')
        $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value {
            Write-Output "[$($this.Timestamp)] $($this.Name) ($($this.Type))"
        }
        Write-Output $result
    }
}



function ConvertTo-TeamViewerConnectionReport {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id                 = $InputObject.id
            UserId             = $InputObject.userid
            UserName           = $InputObject.username
            DeviceId           = $InputObject.deviceid
            DeviceName         = $InputObject.devicename
            GroupId            = $InputObject.groupid
            GroupName          = $InputObject.groupname
            SupportSessionType = [TeamViewerConnectionReportSessionType]$InputObject.support_session_type
            StartDate          = $InputObject.start_date | ConvertTo-DateTime
            EndDate            = $InputObject.end_date | ConvertTo-DateTime
            SessionCode        = $InputObject.session_code
            Fee                = $InputObject.fee
            BillingState       = $InputObject.billing_state
            Currency           = $InputObject.currency
            Notes              = $InputObject.notes
        }

        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.ConnectionReport')
        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
        }

        if ($InputObject.teamviewerPolicyId) {
            $properties["PolicyId"] = [guid]$InputObject.teamviewerPolicyId
        }

        $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
            }
            if ($InputObject.activated_license_id) {
                $properties += @{
                    ActivatedLicenseId      = [guid]$InputObject.activated_license_id
                    ActivatedLicenseName    = $InputObject.activated_license_name
                    ActivatedSubLicenseName = $InputObject.activated_subLicense_name
                }
            }
            if ($InputObject.activated_meeting_license_key) {
                $properties += @{
                    ActivatedMeetingLicenseId = [guid]$InputObject.activated_meeting_license_key
                }
            }
        }
        $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 ConvertTo-TeamViewerUserGroup {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            Id   = [UInt64]$InputObject.id
            Name = $InputObject.name
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.UserGroup')
        Write-Output $result
    }
}



function ConvertTo-TeamViewerUserGroupMember {
    param(
        [Parameter(ValueFromPipeline)]
        [PSObject]
        $InputObject
    )
    process {
        $properties = @{
            AccountId = [int]$InputObject.accountId
            Name      = $InputObject.name
        }
        $result = New-Object -TypeName PSObject -Property $properties
        $result.PSObject.TypeNames.Insert(0, 'TeamViewerPS.UserGroupMember')
        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*\]\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 }
                }
                'int32' {
                    $entry.EntryValue = [int32]($entry.EntryValue)
                }
                'int64' {
                    $entry.EntryValue = [int64]($entry.EntryValue)
                }
            }
            $entry
        }
    }

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



function Get-TeamViewerRegKeyPath {
    param (
        [Parameter()]
        [ValidateSet('WOW6432', 'Auto')]
        [string]
        $Variant = 'Auto'
    )
    if (($Variant -eq 'WOW6432') -Or (Test-TeamViewer32on64)) {
        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

    $currentProgressPreference = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'

    # 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
        $ProgressPreference = $currentProgressPreference
    }
}



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-TeamViewerUserEmail {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $false)]
        [object]
        $User
    )
    Process {
        if (!$User) {
            return $null
        }
        elseif ($User.PSObject.TypeNames -contains 'TeamViewerPS.User') {
            return $User.Email
        }
        elseif ($User -is [string]) {
            return $User
        }
        else {
            throw "Invalid user email '$User'. Must be either a [TeamViewerPS.User] or [string]."
        }
    }
}



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



function Resolve-TeamViewerUserGroupMemberMemberId {
    param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [object]
        $UserGroupMember
    )
    Process {
        if ($UserGroupMember.PSObject.TypeNames -contains 'TeamViewerPS.UserGroupMember') {
            return $UserGroupMember.AccountId
        }
        elseif ($UserGroupMember -is [string]) {
            return [int]$UserGroupMember
        }
        elseif ($UserGroupMember -is [int]) {
            return $UserGroupMember
        }
        else {
            throw "Invalid user group identifier '$UserGroupMember'. Must be either a [TeamViewerPS.UserGroupMember], [int] 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 Test-TeamViewer32on64 {
    if (![Environment]::Is64BitOperatingSystem) {
        return $false
    }
    $registryKey = Get-TeamViewerRegKeyPath -Variant WOW6432
    if (!(Test-Path $registryKey)) {
        return $false
    }
    try {
        $installationDirectory = (Get-Item $registryKey).GetValue('InstallationDirectory')
        $binaryPath = Join-Path $installationDirectory 'TeamViewer.exe'
        return Test-Path $binaryPath
    }
    catch {
        return $false
    }
}



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 = @()
        $null = $ApiToken   # https://github.com/PowerShell/PSScriptAnalyzer/issues/1472

        function Invoke-RequestInternal {
            $body = @{
                emails = @($emailsToAdd)
            }
            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
        }
    }
    Process {
        if ($PSCmdlet.ShouldProcess($Email, "Add SSO exclusion")) {
            $emailsToAdd += $Email
        }
        if ($emailsToAdd.Length -eq 100) {
            Invoke-RequestInternal
            $emailsToAdd = @()
        }
    }
    End {
        if ($emailsToAdd.Length -gt 0) {
            Invoke-RequestInternal
        }
    }
}



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

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [int[]]
        $Member
    )

    Begin {
        $id = $UserGroup | Resolve-TeamViewerUserGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups/$id/members"
        $membersToAdd = @()
        $null = $ApiToken # https://github.com/PowerShell/PSScriptAnalyzer/issues/1472

        function Invoke-TeamViewerRestMethodInternal {
            $result = Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Post `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($membersToAdd | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop
            Write-Output ($result | ConvertTo-TeamViewerUserGroupMember)
        }
    }

    Process {
        # when members are provided as pipline input, each meber is provided as separate statment,
        # thus the members should be combined to one array, otherwise we will send several request
        if ($PSCmdlet.ShouldProcess($Member, "Add user groups member")) {
            $membersToAdd += $Member
        }

        # WebAPI accepts max 100 accounts. Thus we send a request, and reset the `membersToAdd`
        # in order to accept more mebers
        if ($membersToAdd.Length -eq 100) {
            Invoke-TeamViewerRestMethodInternal
            $membersToAdd = @()
        }
    }

    End {
        # A request needs to be send if there were less than 100 members
        if ($membersToAdd.Length -gt 0) {
            Invoke-TeamViewerRestMethodInternal
        }
    }
}



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-TeamViewerConnectionReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $false)]
        [string]
        $UserName,

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $DeviceName,

        [Parameter(Mandatory = $false)]
        [int]
        $DeviceId,

        [Parameter(Mandatory = $false)]
        [switch]
        $WithSessionCode,

        [Parameter(Mandatory = $false)]
        [switch]
        $WithoutSessionCode,

        [Parameter(Mandatory = $false)]
        [string]
        $SessionCode,

        [Parameter(Mandatory = $false)]
        [TeamViewerConnectionReportSessionType]
        $SupportSessionType,

        [Parameter(Mandatory = $true, ParameterSetName = "AbsoluteDates")]
        [DateTime]
        $StartDate,

        [Parameter(Mandatory = $false, ParameterSetName = "AbsoluteDates")]
        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [DateTime]
        $EndDate = (Get-Date),

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 12)]
        [int]
        $Months,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 31)]
        [int]
        $Days,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 24)]
        [int]
        $Hours,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 60)]
        [int]
        $Minutes,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Limit
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/reports/connections";

    $parameters = @{}

    if ($PSCmdlet.ParameterSetName -Eq 'RelativeDates') {
        $StartDate = $EndDate.AddMonths(-1 * $Months).AddDays(-1 * $Days).AddHours(-1 * $Hours).AddMinutes(-1 * $Minutes)
    }
    if ($StartDate -And $EndDate -And $StartDate -lt $EndDate) {
        $parameters.from_date = $StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
        $parameters.to_date = $EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
    }

    if ($UserName) {
        $parameters.username = $UserName
    }

    if ($UserId) {
        $parameters.userid = $UserId | Resolve-TeamViewerUserId
    }

    if ($DeviceName) {
        $parameters.devicename = $DeviceName
    }

    if ($DeviceId) {
        $parameters.deviceid = $DeviceId
    }

    if ($GroupId) {
        $parameters.groupid = $GroupId | Resolve-TeamViewerGroupId
    }

    if ($WithSessionCode -And !$WithoutSessionCode) {
        $parameters.has_code = $true
    }
    elseif ($WithoutSessionCode -And !$WithSessionCode) {
        $parameters.has_code = $false
    }

    if ($SessionCode) {
        $parameters.session_code = $SessionCode
    }

    if ($SupportSessionType) {
        $parameters.support_session_type = [int]$SupportSessionType
    }

    $remaining = $Limit
    do {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Get `
            -Body $parameters `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop
        $results = ($response.records | ConvertTo-TeamViewerConnectionReport)
        if ($Limit) {
            Write-Output ($results | Select-Object -First $remaining)
            $remaining = $remaining - @($results).Count
        }
        else {
            Write-Output $results
        }
        $parameters.offset_id = $response.next_offset
    } while ($parameters.offset_id -And (!$Limit -Or $remaining -gt 0))
}



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-TeamViewerEventLog {
    [CmdletBinding(DefaultParameterSetName = "RelativeDates")]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true, ParameterSetName = "AbsoluteDates")]
        [DateTime]
        $StartDate,

        [Parameter(Mandatory = $false, ParameterSetName = "AbsoluteDates")]
        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [DateTime]
        $EndDate = (Get-Date),

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 12)]
        [int]
        $Months,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 31)]
        [int]
        $Days,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 24)]
        [int]
        $Hours,

        [Parameter(Mandatory = $false, ParameterSetName = "RelativeDates")]
        [ValidateRange(0, 60)]
        [int]
        $Minutes,

        [Parameter(Mandatory = $false)]
        [int]
        $Limit,

        [Parameter(Mandatory = $false)]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                $null = @($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                @(
                    'AddRemoteWorkerDevice',
                    'ChangedDisabledRemoteInput',
                    'ChangedShowBlackScreen',
                    'CompanyAddressBookDisabled',
                    'CompanyAddressBookEnabled',
                    'CompanyAddressBookMembersHid',
                    'CompanyAddressBookMembersUnhid'
                    'ConditionalAccessBlockMeetingStateChanged',
                    'ConditionalAccessDirectoryGroupAdded',
                    'ConditionalAccessDirectoryGroupDeleted',
                    'ConditionalAccessDirectoryGroupMembersAdded',
                    'ConditionalAccessDirectoryGroupMembersDeleted',
                    'ConditionalAccessRuleAdded',
                    'ConditionalAccessRuleDeleted',
                    'ConditionalAccessRuleModified',
                    'ConditionalAccessRuleVerificationStateChanged',
                    'CreateCustomHost',
                    'DeleteCustomHost',
                    'EditOwnProfile',
                    'EditTFAUsage',
                    'EditUserPermissions',
                    'EditUserProperties',
                    'EmailConfirmed',
                    'EndedRecording',
                    'EndedSession',
                    'GroupAdded',
                    'GroupDeleted',
                    'GroupShared',
                    'GroupUpdated',
                    'IncomingSession',
                    'JoinCompany',
                    'JoinedSession',
                    'LeftSession',
                    'ParticipantJoinedSession',
                    'ParticipantLeftSession',
                    'PausedRecording',
                    'PolicyAdded',
                    'PolicyDeleted',
                    'PolicyUpdated',
                    'ReceivedDisabledLocalInput',
                    'ReceivedFile',
                    'ReceivedShowBlackScreen',
                    'RemoveRemoteWorkerDevice',
                    'ResumedRecording',
                    'ScriptTokenAdded',
                    'ScriptTokenDeleted',
                    'ScriptTokenUpdated',
                    'SentFile',
                    'StartedRecording',
                    'StartedSession',
                    'SwitchedSides',
                    'UpdateCustomHost',
                    'UserCreated',
                    'UserDeleted',
                    'UserGroupCreated',
                    'UserGroupDeleted',
                    'UserGroupMembersAdded',
                    'UserGroupMembersRemoved',
                    'UserGroupUpdated',
                    'UserRemovedFromCompany'
                ) | Where-Object { $_ -like "$wordToComplete*" }
            } )]
        [string[]]
        $EventNames,

        [Parameter(Mandatory = $false)]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                $null = @($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                @(
                    "CompanyAddressBook",
                    "CompanyAdministration",
                    "ConditionalAccess",
                    "CustomModules",
                    "GroupManagement",
                    "LicenseManagement",
                    "Policy",
                    "Session",
                    "UserGroups",
                    "UserProfile"
                ) | Where-Object { $_ -like "$wordToComplete*" }
            })]
        [string[]]
        $EventTypes,

        [Parameter(Mandatory = $false)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserEmail } )]
        [Alias("Users")]
        [object[]]
        $AccountEmails,

        [Parameter(Mandatory = $false)]
        [string]
        $AffectedItem,

        [Parameter(Mandatory = $false)]
        [Alias("RemoteControlSession")]
        [guid]
        $RemoteControlSessionId
    )

    $resourceUri = "$(Get-TeamViewerApiUri)/EventLogging";

    $Limit = if ($Limit -lt 0) { $null } else { $Limit }

    if ($PSCmdlet.ParameterSetName -Eq 'RelativeDates') {
        $Hours = if (!$Months -And !$Days -And !$Hours -And !$Minutes) { 1 } else { $Hours }
        $StartDate = $EndDate.AddMonths(-1 * $Months).AddDays(-1 * $Days).AddHours(-1 * $Hours).AddMinutes(-1 * $Minutes)
    }

    $parameters = @{
        StartDate = $StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
        EndDate   = $EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
    }

    if ($EventNames) {
        $parameters.EventNames = $EventNames
    }
    if ($EventTypes) {
        $parameters.EventTypes = $EventTypes
    }
    if ($AccountEmails) {
        $parameters.AccountEmails = @($AccountEmails | Resolve-TeamViewerUserEmail)
    }
    if ($AffectedItem) {
        $parameters.AffectedItem = $AffectedItem
    }
    if ($RemoteControlSessionId) {
        $parameters.RCSessionGuid = $RemoteControlSessionId
    }

    $remaining = $Limit
    do {
        $response = Invoke-TeamViewerRestMethod `
            -ApiToken $ApiToken `
            -Uri $resourceUri `
            -Method Post `
            -ContentType "application/json; charset=utf-8" `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($parameters | ConvertTo-Json))) `
            -WriteErrorTo $PSCmdlet `
            -ErrorAction Stop
        $results = ($response.AuditEvents | ConvertTo-TeamViewerAuditEvent)
        if ($Limit) {
            Write-Output ($results | Select-Object -First $remaining)
            $remaining = $remaining - @($results).Count
        }
        else {
            Write-Output $results
        }
        $parameters.ContinuationToken = $response.ContinuationToken
    } while ($parameters.ContinuationToken -And (!$Limit -Or $remaining -gt 0))
}



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")]
        [Alias("Device")]
        [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,

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

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

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

    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 -In @('List', 'ByDeviceId') `
            -And $parameters.paginationToken)
}



function Get-TeamViewerManagementId {
    if (Test-TeamViewerInstallation) {
        switch (Get-OperatingSystem) {
            'Windows' {
                $regKeyPath = Join-Path (Get-TeamViewerRegKeyPath) 'DeviceManagementV2'
                $regKey = if (Test-Path -LiteralPath $regKeyPath) { Get-Item -Path $regKeyPath }
                if ($regKey) {
                    $unmanaged = [bool]($regKey.GetValue('Unmanaged'))
                    $managementId = $regKey.GetValue('ManagementId')
                }
            }
            'Linux' {
                $unmanaged = [bool](Get-TeamViewerLinuxGlobalConfig -Name 'DeviceManagementV2\Unmanaged')
                $managementId = Get-TeamViewerLinuxGlobalConfig -Name 'DeviceManagementV2\ManagementId'
            }
        }
        if (!$unmanaged -And $managementId) {
            Write-Output ([guid]$managementId)
        }
    }
}



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-TeamViewerUserGroup {
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter()]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup
    )

    Begin {
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups"
        $parameters = @{ }
        $isListOperation = $true

        if ($UserGroup) {
            $GroupId = $UserGroup | Resolve-TeamViewerUserGroupId
            $resourceUri += "/$GroupId"
            $parameters = $null
            $isListOperation = $false
        }
    }

    Process {
        do {
            $response = Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Get `
                -Body $parameters `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop
            if ($UserGroup) {
                Write-Output ($response | ConvertTo-TeamViewerUserGroup)
            }
            else {
                $parameters.paginationToken = $response.nextPaginationToken
                Write-Output ($response.resources | ConvertTo-TeamViewerUserGroup)
            }
        } while ($isListOperation -And $parameters.paginationToken)
    }
}



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

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup
    )

    Begin {
        $id = $UserGroup | Resolve-TeamViewerUserGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups/$id/members"
        $parameters = @{ }
    }

    Process {
        do {
            $response = Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Get `
                -Body $parameters `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop
            $parameters.paginationToken = $response.nextPaginationToken
            Write-Output ($response.resources | ConvertTo-TeamViewerUserGroupMember)
        } while ($parameters.paginationToken)
    }
}



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', 'Full64Bit')]
        [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' }
                'Full64Bit' { 'TeamViewer_Setup_x64.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 New-TeamViewerUserGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

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

    Begin {
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups"
        $body = @{ name = $Name }
    }

    Process {
        if ($PSCmdlet.ShouldProcess($Name, "Create user 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-TeamViewerUserGroup)
        }
    }
}



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(Mandatory = $true)]
        [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 = @()
        $null = $ApiToken   # https://github.com/PowerShell/PSScriptAnalyzer/issues/1472

        function Invoke-RequestInternal {
            $body = @{
                emails = @($emailsToRemove)
            }
            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
        }
    }
    Process {
        if ($PSCmdlet.ShouldProcess($Email, "Remove SSO exclusion")) {
            $emailsToRemove += $Email
        }
        if ($emailsToRemove.Length -eq 100) {
            Invoke-RequestInternal
            $emailsToRemove = @()
        }
    }
    End {
        if ($emailsToRemove.Length -gt 0) {
            Invoke-RequestInternal
        }
    }
}



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

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup
    )

    Begin {
        $id = $UserGroup | Resolve-TeamViewerUserGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups/$id"
    }

    Process {
        if ($PSCmdlet.ShouldProcess($UserGroup.ToString(), "Remove user group")) {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }
    }
}



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

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup,

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

        [Parameter(Mandatory = $true, ParameterSetName = "ByUserGroupMemberId", ValueFromPipeline = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupMemberMemberId } )]
        [Alias("UserGroupMemberId")]
        [Alias("MemberId")]
        [object[]]
        $UserGroupMember
    )

    Begin {
        $id = $UserGroup | Resolve-TeamViewerUserGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups/$id/members"
        $membersToRemove = @()
        $null = $ApiToken # https://github.com/PowerShell/PSScriptAnalyzer/issues/1472
        $null = $User
        $null = $UserGroupMember
        function Invoke-TeamViewerRestMethodForMember {
            Invoke-TeamViewerRestMethod `
                -ApiToken $ApiToken `
                -Uri $resourceUri `
                -Method Delete `
                -ContentType "application/json; charset=utf-8" `
                -Body ([System.Text.Encoding]::UTF8.GetBytes(($membersToRemove | ConvertTo-Json))) `
                -WriteErrorTo $PSCmdlet `
                -ErrorAction Stop | `
                Out-Null
        }

        function Get-Target {
            switch ($PsCmdlet.ParameterSetName) {
                'ByUserId'  { return $User.ToString() }
                Default     { return $UserGroupMember.ToString() }
            }
        }

        function Get-MemberId {
            switch ($PsCmdlet.ParameterSetName) {
                'ByUserId' {
                    $UserId = $User | Resolve-TeamViewerUserId
                    $UserId.TrimStart('u')
                }
                'ByUserGroupMemberId'{
                    return $UserGroupMember | Resolve-TeamViewerUserGroupMemberMemberId
                }
            }
        }
    }

    Process {
        # when members are provided as pipline input, each meber is provided as separate statment,
        # thus the members should be combined to one array, otherwise we will send several request
        if ($PSCmdlet.ShouldProcess((Get-Target), "Remove user group member")) {
            $membersToRemove += Get-MemberId
        }

        # WebAPI accepts max 100 accounts. Thus we send a request, and reset the `membersToAdd`
        # in order to accept more mebers
        if ($membersToRemove.Length -eq 100) {
            Invoke-TeamViewerRestMethodForMember
            $membersToRemove = @()
        }
    }

    End {
        # A request needs to be send if there were less than 100 members
        if ($membersToRemove.Length -gt 0) {
            Invoke-TeamViewerRestMethodForMember
        }
    }
}



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-TeamViewerManagedDevice {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

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

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

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

        [switch]
        $RemovePolicy
    )
    Begin {
        $body = @{}

        if ($Name) {
            $body['name'] = $Name
        }
        if ($Policy) {
            $body['teamviewerPolicyId'] = $Policy | Resolve-TeamViewerPolicyId
        }
        elseif ($RemovePolicy) {
            $body['teamviewerPolicyId'] = ""
        }

        if ($Policy -And $RemovePolicy) {
            $PSCmdlet.ThrowTerminatingError(
                ("Parameters -Policy and -RemovePolicy cannot be used together." | `
                        ConvertTo-ErrorRecord -ErrorCategory InvalidArgument))
        }

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

        if ($PSCmdlet.ShouldProcess($Device.ToString(), "Change managed 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-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 -Depth 25))) `
            -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 Set-TeamViewerUserGroup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]
        $ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { $_ | Resolve-TeamViewerUserGroupId } )]
        [Alias("UserGroupId")]
        [Alias("Id")]
        [object]
        $UserGroup,

        [Parameter(Mandatory = $true)]
        [Alias("UserGroupName")]
        [string]
        $Name
    )
    Begin {
        $id = $UserGroup | Resolve-TeamViewerUserGroupId
        $resourceUri = "$(Get-TeamViewerApiUri)/usergroups/$id"
        $body = @{ name = $Name }
    }
    Process {
        if ($PSCmdlet.ShouldProcess($UserGroup.ToString(), "Change user group")) {
            $response = 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
            Write-Output ($response | ConvertTo-TeamViewerUserGroup)
        }
    }
}



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' {
            $regKey = Get-TeamViewerRegKeyPath
            $installationDirectory = if (Test-Path $regKey) { (Get-Item $regKey).GetValue('InstallationDirectory') }
            Write-Output (
                $installationDirectory -And `
                (Test-Path "$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
    }
}