<?xml version="1.0" encoding="utf-8"?>
<AlvaoApplication xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ModelVersion="1">
  <Applications>
    <Application id="41">
      <Name>TeamViewer Connector</Name>
      <Description>This connector enables IT service desk agents and asset managers to launch TeamViewer remote desktop sessions directly from ALVAO tickets or objects (assets).</Description>
      <UniqueId>862883d5-91b4-4026-bb60-c9833f12d15e</UniqueId>
      <Version>1</Version>
      <AdvancedSettings>
        <Setting>
          <Name>TeamViewer.Token</Name>
          <Value />
        </Setting>
      </AdvancedSettings>
      <Scripts>
        <Script id="1136">
          <Name>TeamViewerConnectorCommon</Name>
          <Code>using Alvao.API.Internal;
using NLog;

namespace Alvao.CustomApps.TeamViewerConnector;

public static class TeamViewerConnectorCommon
{
    public const string ConnectorName = "TeamViewer";

    public const string ApiBaseUrl = "https://webapi.teamviewer.com/api/v1/";

    public const string UnattendedSessionLogText = "TeamViewer Connector: Unattended session initiated.";

    public const string AttendedSessionRequestedLogText = "TeamViewer Connector: Attended session requested.";

    public static ILogger Log { get; } = TenantDiagnosticsLog.Get();
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>MV8xcL4ei5CBa7CxrxJiF5jJxfmTJfd/s5jyE0xWLzSVE/Dl9UAu/INLEkI9wkO81jvZMubGoQoYN94B8TrGXeaUXbOKT1dKgi801P1sG3Pv34KtA0z11ExMQhhXhDwapF1Eupll9ShjnUw12I8IzcXvZAi2AHYRQ8LWfSa+nl/OqklCzesNuTOeGLHJ0/ZSgOc0HUCqSBES1x4f1Y8qt3G1iX+ENtO8vBuM5/03sfApCpk6EUGi7euIvdhkjDUUmkm6Y/w80fLRKzyML8piL60i+DvF+JZckow9CsesbGyVlNE2C5Y92JBAJ8jjU0BLSaz0Wky9u6nw/M22kTFu2w==</Codesign>
        </Script>
        <Script id="1137">
          <Name>OpenObjectInTeamViewerEntityCommand</Name>
          <Code>using System;
using Alvao.API.Common;
using Alvao.API.Common.Model.CustomApps;
using Alvao.API.Common.Model.Database;
using Alvao.Apps.API;
using Alvao.Context;
using Dapper;

namespace Alvao.CustomApps.TeamViewerConnector;

public class OpenObjectInTeamViewerEntityCommand : IEntityCommand
{
    public string Id { get; set; }

    public Entity Entity { get; set; }

    public OpenObjectInTeamViewerEntityCommand()
    {
        Id = "OpenObjectInTeamViewerEntityCommand";
        Entity = Entity.Object;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {
        const int position = 2;
        const string icon = "open_20_regular";
        const string name = "Open in TeamViewer";

        return new EntityCommandShowResult(CanShow(entityId, personId), name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        string teamViewerGuid = GetTeamViewerGuid(entityId);

        if (string.IsNullOrWhiteSpace(teamViewerGuid))
        {
            return new CommandResult(MessageType.Error,"The TeamViewer device identifier is missing.",null);
        }

        string navigateToUrl = $"https://web.teamviewer.com/devices/basic/{Uri.EscapeDataString(teamViewerGuid.Trim())}/details";

        return new CommandResult(MessageType.None, null, navigateToUrl);
    }

    private static string GetTeamViewerGuid(int objectId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.ExecuteScalar&lt;string&gt;(@"
            select TeamViewerGuid
            from NodeCust
            where NodeId = @objectId", new { objectId }, transaction: scope.Transaction);
    }

    private static bool CanShow(int objectId, int personId)
    {
        if (string.IsNullOrWhiteSpace(GetTeamViewerGuid(objectId)))
        {
            return false;
        }

        return PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetManagers, false) || PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetAdministrators, false);
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>PSJeeUheHZYkMefMKR/z2R7LHZpRZLwjoTQ/V2sU2RaDlshMbvnhM+mXGvm+1L8FW43B83dHMPVinEMvxXTXByf0ShgNce5x1sSRl4qDyF1r02lDx7plryvSB3DyJD0WhZTJAZA+dWV8389C3njjDBg8HAXe4RJof1DUslrLlDR9/8qGtrarzULuibOEPdS7x5gMZUCX+kAsXGqc6edF2uMhRVV26hNT/sfhfgZg38PPoQqRzA4KZ0u/Zyk0dgu4F6oMiD02a7sfzIJo5qVxPWG7jPe1WO0aY7taW4KkmImScLoF9BSCl/vm+udv92Irg1K7qkj0/B1x9O6ZM6c9Gg==</Codesign>
        </Script>
        <Script id="1138">
          <Name>StartTeamViewerSessionEntityCommand</Name>
          <Code>using System;
using Alvao.API.Common;
using Alvao.API.Common.Model.CustomApps;
using Alvao.API.Common.Model.Database;
using Alvao.Apps.API;
using Alvao.Context;
using Dapper;

namespace Alvao.CustomApps.TeamViewerConnector;

public class StartTeamViewerSessionEntityCommand : IEntityCommand
{
    public string Id { get; set; }

    public Entity Entity { get; set; }

    public StartTeamViewerSessionEntityCommand()
    {
        Id = "StartTeamViewerSessionEntityCommand";
        Entity = Entity.Object;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {
        const int position = 2;
        const string icon = "wifi_1_20_regular";
        const string name = "TeamViewer session";

        return new EntityCommandShowResult(CanShow(entityId, personId), name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        string teamViewerDeviceId = GetTeamViewerDeviceId(entityId);

        if (string.IsNullOrWhiteSpace(teamViewerDeviceId))
        {
            return new CommandResult(MessageType.Error, "The TeamViewer device ID is missing.", null);
        }

        string normalizedDeviceId = teamViewerDeviceId.Replace(" ", string.Empty).Replace("\u00A0", string.Empty).Trim();

        string navigateToUrl = $"https://start.teamviewer.com/{Uri.EscapeDataString(normalizedDeviceId)}#?Popup=True";

        LogConnection(entityId, personId);

        return new CommandResult(MessageType.None, null, navigateToUrl);
    }

    private static string GetTeamViewerDeviceId(int objectId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.ExecuteScalar&lt;string&gt;(@"
            select TeamViewerDeviceId
            from NodeCust
            where NodeId = @objectId", new { objectId }, transaction: scope.Transaction);
    }

    private static void LogConnection(int objectId, int personId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute(@"
            insert into tblLog
            (
                lintNodeId,
                liLogPersonId,
                dteLog,
                txtLog,
                lintLogKindId
            )
            values
            (
                @objectId,
                @personId,
                @date,
                @text,
                1
            )", new { objectId, personId, date = DateTime.UtcNow, text = TeamViewerConnectorCommon.UnattendedSessionLogText }, transaction: scope.Transaction);
    }

    private static bool CanShow(int objectId, int personId)
    {
        if (string.IsNullOrWhiteSpace(GetTeamViewerDeviceId(objectId)))
        {
            return false;
        }

        return PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetManagers, false) || PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetAdministrators, false);
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>YUXXBCsNzkUMkYmT2O9L5wwdClOO1XNVNtGtJi399p7WmT8upyU9Ah9KPn40/GFothQMMJ6HTSXc2b+iykxKTjU5+JxARtyCBzIDxDaCU/j+qR8YNZe6TYFg8CozigkugGNJFOc+4G59qWrFy0py5muPZ07WpRdql9NZWFd5q2WWHOaVJxfv85XGJu3b6beDFqzmg1cUxc25o3oNYlFSOLk4RlXwIxc9hI2VKmzCzfQL9ZocwcIpTc1viUaxv28bZh3AKBj6DwHt35YZPv+Uz2P0EHUU8Sfx/SdYHLGlRj3Rp2iBcuLkDaQFgBKdFHT6Y4gRdW+5dtDm5WDD3AZQZA==</Codesign>
        </Script>
        <Script id="1139">
          <Name>RequestTeamViewerSessionViaTeamsEntityCommand</Name>
          <Code>using System;
using System.Linq;
using Alvao.API.Common.Model;
using Alvao.API.Common.Model.CustomApps;
using Alvao.API.Common.Model.Database;
using Alvao.API.SD;
using Alvao.API.SD.Model;
using Alvao.Apps.API;
using Alvao.Context;
using Alvao.API.Common;
using Dapper;

namespace Alvao.CustomApps.TeamViewerConnector;

public class RequestTeamViewerSessionViaTeamsEntityCommand : IEntityCommand
{
    public string Id { get; set; }

    public Entity Entity { get; set; }

    public RequestTeamViewerSessionViaTeamsEntityCommand()
    {
        Id = "RequestTeamViewerSessionViaTeamsEntityCommand";
        Entity = Entity.Request;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {
        const int position = 4;
        const string icon = "mail_20_regular";
        const string name = "Request TeamViewer session via Teams";

        return new EntityCommandShowResult(CanShow(entityId, personId), name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        string requesterEmail = GetRequesterEmail(entityId);

        if (string.IsNullOrWhiteSpace(requesterEmail))
        {
            return new CommandResult(MessageType.Error, "The ticket requester does not have an email address.", null);
        }

        int objectId = GetLinkedComputerId(entityId);

        if (objectId == 0)
        {
            return new CommandResult(MessageType.Error, "The ticket must have exactly one linked computer.", null);
        }

        string message = GetMessage(entityId);

        string navigateToUrl = $"https://teams.microsoft.com/l/chat/0/0" + $"?users={Uri.EscapeDataString(requesterEmail)}" + $"&amp;message={Uri.EscapeDataString(message)}";

        LogRequestToTicket(entityId, personId);
        LogRequestToObject(objectId, personId);

        return new CommandResult(MessageType.None, null, navigateToUrl);
    }

    private static bool CanShow(int ticketId, int personId)
    {
        if (!IsSolverTeamMember(ticketId, personId))
        {
            return false;
        }

        if (GetLinkedComputerId(ticketId) == 0)
        {
            return false;
        }

        string requesterEmail = GetRequesterEmail(ticketId);

        return !string.IsNullOrWhiteSpace(requesterEmail);
    }

    private static bool IsSolverTeamMember(int ticketId, int personId)
    {
        return PersonRights.IsTicketSolverTeamMember(personId, ticketId);
    }

    private static int GetLinkedComputerId(int ticketId)
    {
        int[] computerIds = Alvao.API.SD.Relation.GetLinkedNodes(ticketId).Where(Alvao.API.AM.Object.IsComputer).Take(2).ToArray();

        return computerIds.Length == 1
            ? computerIds[0] : 0;
    }

    private static string GetRequesterEmail(int ticketId)
    {
        var ticket = Alvao.API.SD.Ticket.GetById(ticketId);

        if (ticket == null || ticket.liHdTicketUserPersonId &lt;= 0)
        {
            return null;
        }

        return Alvao.API.Common.Person.GetById(ticket.liHdTicketUserPersonId)
            ?.sPersonEmail;
    }

    private static string GetMessage(int ticketId)
    {
        int requesterId = Alvao.API.SD.Ticket.GetById(ticketId).liHdTicketUserPersonId;

        int preferredLanguage = Alvao.API.Common.Person.GetCultureInfoOrDefaultFromId(requesterId).LCID;

        if (preferredLanguage == 1029)
        {
            return "Ahoj,\r\n\r\n" +
                "Prosím otevři odkaz " +
                "https://www.teamviewer.com/en/solutions/use-cases/quicksupport/\r\n\r\n" +
                "Stáhni a spusť QuickSupport z odkazu. " +
                "Zobrazí se ti devítimístné ID a heslo.\r\n\r\n" +
                "Nech program spuštěný a pošli mi ID a heslo, " +
                "abych se mohl připojit k tvému zařízení.\r\n\r\n" +
                "Děkuji,\r\n" +
                "Administrátor";
        }

        return "Hi,\r\n\r\n" +
            "Please open the link " +
            "https://www.teamviewer.com/en/solutions/use-cases/quicksupport/\r\n\r\n" +
            "Download and run the QuickSupport from the link. " +
            "It will show you a 9-digit ID and password.\r\n\r\n" +
            "Keep the program running and send me the ID and password " +
            "so I can connect to your device.\r\n\r\n" +
            "Thank you,\r\n" +
            "Administrator";
    }

    private static void LogRequestToTicket(int ticketId, int personId)
    {
        const string subject = "TeamViewer Connector: Attended session requested.";

        var settings = new ActCreateSettings
        {
            ShowToRequester = false
        };

        Act.Create(ticketId, subject, new HtmlTextModel(subject), personId, null, tActKind.ActKind.Note, settings);
    }

    private static void LogRequestToObject(int objectId, int personId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute(@"
            insert into tblLog
            (
                lintNodeId,
                liLogPersonId,
                dteLog,
                txtLog,
                lintLogKindId
            )
            values
            (
                @objectId,
                @personId,
                @date,
                @text,
                1
            )", new { objectId, personId, date = DateTime.UtcNow, text = TeamViewerConnectorCommon .AttendedSessionRequestedLogText }, transaction: scope.Transaction);
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>tfN5wRjOoxUkCUJ6d7omzsqe6N2ryenv/ENCfBDheikys27tgbRnD/PiXXsDASTjCVHhp/3iMDFTNiL1Tja0EMbIXvLHqObIX7AX9Mq6Y3tDwDgbpfOpICBUc3/Sqkv9c6NqAgtNYYX9w/LLurM8se8bSFY4ArdJLzalA1Jmq7lWtRb/zTs49YMSvy3RA9MtyoKL+JllhF8qlMrv47gZPI5paeWJmFvboLCeBNtIXQGshvgeekWCKZxu+F61H5sqjJ18xKixJ3fKiZw0e7W+i7P7VALPvgSt7oGAxHIGoI+TdBJL3wr4oADIE5s4rm9/O2I3Hql6s9wbUpqpKr+sgw==</Codesign>
        </Script>
        <Script id="1140">
          <Name>TeamViewerConnectorPeriodicAction</Name>
          <Code>using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Alvao.API.Common;
using Alvao.Apps.API;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Collections.Generic;
using Alvao.Context;
using Dapper;
using Alvao.API.Common.Model.Database;
using Alvao.API.Common.Model.CustomApps.Requests;
using Alvao.API.AM.Model;
using System.Text.RegularExpressions;
using static Alvao.API.AM.Model.Kind;
using static Alvao.Global.ModuleInfo;

namespace Alvao.CustomApps.TeamViewerConnector;

internal class TeamViewerConnectorRepository
{
    private readonly List&lt;int&gt; scannerPropertyMapCache = new List&lt;int&gt;();

    public tblNode GetDeviceNodeByProperty(tblKind.KindCode kindCode, string propertyValue, bool onlyComputers)
    {
        if (string.IsNullOrWhiteSpace(propertyValue))
        {
            return null;
        }

        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.Query&lt;tblNode&gt;(@"
            declare @columnName nvarchar(255);

            select @columnName = k.ColumnName
            from tblKind k
            where k.intKindCode = @kindCode;

            if @columnName is null
                return;

            declare @sql nvarchar(max);

            set @sql = N'
                select top (1)
                    n.intNodeId,
                    n.lintClassId,
                    n.ImportDuplicateAlert
                from tblNode n
                ' + case
                    when @onlyComputers = 1
                    then N'
                        join tblClass c
                            on c.intClassId = n.lintClassId
                        and c.bComputer = 1'
                    else N''
                end + N'
                join ClassKind ck
                    on ck.ClassId = n.lintClassId
                join tblKind k
                    on k.intKindId = ck.KindId
                and k.intKindCode = @kindCode
                join NodeCust nc
                    on nc.NodeId = n.intNodeId
                where n.IsHidden = 0
                and ltrim(rtrim(
                    convert(nvarchar(4000), nc.'
                    + quotename(@columnName)
                    + N'))) = @propertyValue
                order by n.intNodeId asc;';

            exec sp_executesql
                @sql,
                N'@kindCode int, @propertyValue nvarchar(4000)',
                @kindCode,
                @propertyValue;
            ", new { kindCode = (int)kindCode, propertyValue = propertyValue.Trim(), onlyComputers }, transaction: scope.Transaction).FirstOrDefault();
    }


    public int GetOrCreateImportedObjectsFolder()
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.ExecuteScalar&lt;int&gt;(@"
            declare @nodeId int;

            select top (1)
                @nodeId = n.intNodeId
            from tblNode n
            where n.lintClassId = @classId
            and n.IsActive = 1;

            if @nodeId is null
            begin
                insert into tblNode
                (
                    lintIconId,
                    intState,
                    txtName,
                    lintClassId
                )
                select
                    i.intIconId,
                    128,
                    d.txtText,
                    d.lintClassId
                from tblDict d
                join tblIcon i
                    on i.uid = @iconUid
                where d.lintClassId = @classId;

                select @nodeId = scope_identity();

                insert into tblNodeParent
                (
                    lintNodeId,
                    lintParentNodeId
                )
                values
                (
                    @nodeId,
                    @nodeId
                );

                update nc
                set nc.[Name] = n.txtName
                from NodeCust nc
                join tblNode n
                    on n.intNodeId = nc.NodeId
                where nc.NodeId = @nodeId;
            end;

            select @nodeId;
            ", new { classId = tblClass.ClassCode.ImportedObjects, iconUid = tblIcon.IconUid.Subnet }, transaction: scope.Transaction);
    }

    public tblNode CreateSimpleComputer(int classId, int parentNodeId, string hostname, int personId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.QueryFirst&lt;tblNode&gt;(@"
            declare @pcNodeId int;
            declare @setNodeId int;

            exec Internal.spCreateSimpleComputer
                @hostname,
                @parentNodeId,
                @personId,
                @classId,
                1,
                @pcNodeId output,
                @setNodeId output;

            select
                @pcNodeId as intNodeId,
                @classId as lintClassId;
            ", new { hostname, parentNodeId, personId, classId }, transaction: scope.Transaction);
    }

    public tblNode CreateObjectByClass(int classId, int parentNodeId, int personId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.QueryFirst&lt;tblNode&gt;(@"
            declare @nodeId int;

            exec @nodeId = spCreateNodeFromTemplate
                @classId,
                '',
                @parentNodeId,
                @personId;

            select
                @nodeId as intNodeId,
                @classId as lintClassId;
            ", new { classId, parentNodeId, personId }, transaction: scope.Transaction);
    }

    public void AddMissingPropertiesToAmTemplate(int classId, string kindCodes)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute(@"
            if object_id('tempdb..#kinds') is not null
                drop table #kinds;

            create table #kinds
            (
                id int primary key
            );

            insert into #kinds (id)
            select id
            from dbo.ftCommaListToTableIds(@kindCodes);

            insert into ClassKind
            (
                KindId,
                ClassId
            )
            select
                k.intKindId,
                @classId
            from #kinds ids
            join tblKind k
                on k.intKindCode = ids.id
            left join ClassKind ck
                on ck.ClassId = @classId
            and ck.KindId = k.intKindId
            where ck.KindId is null;
            ", new { classId, kindCodes }, transaction: scope.Transaction);
    }

    public void InsertIntoObjectLog(tblLog log)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute(@"
            insert into tblLog
            (
                lintNodeId,
                liLogPersonId,
                dteLog,
                txtLog,
                lintLogKindId
            )
            values
            (
                @lintNodeId,
                @liLogPersonId,
                @dteLog,
                @txtLog,
                1
            );
            ", new { log.lintNodeId, log.liLogPersonId, log.dteLog, log.txtLog }, transaction: scope.Transaction);
    }

    public void SetImportConflict(int nodeId, bool hasConflict)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute(@"
            update tblNode
            set ImportDuplicateAlert = @hasConflict
            where intNodeId = @nodeId;
            ", new { nodeId, hasConflict }, transaction: scope.Transaction);
    }

    public void ClearScannerPropertyLockoutTable()
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute($@"
            delete spl
            from ScannerPropertyLockout spl
            left join ClassKind ck
                on ck.KindId = spl.LockingKindId
            and ck.ClassId = spl.ClassId
            where spl.ScannerId = {(int)ScannerId.TeamViewer}
            and ck.KindId is null;
            ", transaction: scope.Transaction);
    }

    public void RecalculateScannerPropertyLockoutTable(int classId)
    {
        if (classId &lt;= 0)
        {
            return;
        }

        if (scannerPropertyMapCache.Contains(classId))
        {
            return;
        }

        int lockingKindCode = (int)KindCode.TeamViewerGuid;

        string lockedKindCodes = string.Join(",", new[] { (int)KindCode.TeamViewerGuid, (int)KindCode.HostName, (int)KindCode.TeamViewerDeviceId, (int)KindCode.TeamViewerLastCheckIn, (int)KindCode.Manufacturer, (int)KindCode.RAMCapacityInGB, (int)KindCode.TotalStorageSpaceInGB });

        RecalculateScannerPropertyLockoutByClass(classId, lockingKindCode, lockedKindCodes);

        scannerPropertyMapCache.Add(classId);
    }

    private void RecalculateScannerPropertyLockoutByClass(int classId, int lockingKindCode, string lockedKindCodes)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        scope.Connection.Execute($@"
            if object_id('tempdb..#locked') is not null
                drop table #locked;

            create table #locked
            (
                id int primary key
            );

            insert into #locked (id)
            select id
            from dbo.ftCommaListToTableIds(
                @lockedKindCodes);

            merge into ScannerPropertyLockout as target
            using
            (
                select
                    {(int)ScannerId.TeamViewer} as ScannerId,
                    @classId as ClassId,
                    locking.intKindId as LockingKindId,
                    locked.intKindId as LockedKindId
                from tblKind locked
                join #locked items
                    on items.id = locked.intKindCode
                join tblKind locking
                    on locking.intKindCode =
                        @lockingKindCode
            ) as source
            on target.ScannerId = source.ScannerId
        and target.ClassId = source.ClassId
        and target.LockingKindId =
            source.LockingKindId
        and target.LockedKindId =
            source.LockedKindId

            when not matched by target then
                insert
                (
                    ScannerId,
                    ClassId,
                    LockingKindId,
                    LockedKindId
                )
                values
                (
                    source.ScannerId,
                    source.ClassId,
                    source.LockingKindId,
                    source.LockedKindId
                )

            when not matched by source
                and target.ClassId = @classId
                and target.ScannerId =
                    {(int)ScannerId.TeamViewer}
            then delete;
            ", new { classId, lockingKindCode, lockedKindCodes }, transaction: scope.Transaction);
    }

    public int? GetKindId(tblKind.KindCode kindCode)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.ExecuteScalar&lt;int?&gt;(@"
            select intKindId
            from tblKind
            where intKindCode = @kindCode;
            ", new { kindCode = (int)kindCode }, transaction: scope.Transaction);
    }

}

internal enum TeamViewerOnlineState
{
    Online = 0,
    Busy = 1,
    Away = 2,
    Unknown = 3,
    Offline = 4
}

internal enum TeamViewerHardwareType
{
    Unknown = 0,
    Network = 1,
    Bios = 2,
    Motherboard = 4,
    Keyboard = 5,
    PointingDevice = 6,
    Monitor = 7,
    VideoController = 8,
    DiskDrive = 9,
    PhysicalMemory = 11,
    Processor = 13
}

internal class TeamViewerDevice
{
    public string ManagementId { get; set; }

    public string ContactDeviceId { get; set; }

    public string TeamViewerId { get; set; }

    public string Name { get; set; }

    public TeamViewerOnlineState? OnlineState { get; set; }

    public DateTime? LastCheckInUtc { get; set; }

    public HashSet&lt;string&gt; Sources { get; set; }

    public int? NodeId { get; set; }

    public string Manufacturer { get; set; }

    public double? RamCapacityInGb { get; set; }

    public double? TotalStorageCapacityInGb { get; set; }

    public string BiosSerialNumber { get; set; }

    public Dictionary&lt;tblKind.KindCode, object&gt; GetPropertyMapping()
    {
        var properties = new Dictionary&lt;tblKind.KindCode, object&gt;();

        if (!string.IsNullOrWhiteSpace(ManagementId))
        {
            properties.Add(tblKind.KindCode.TeamViewerGuid, ManagementId);
        }

        if (!string.IsNullOrWhiteSpace(Name))
        {
            properties.Add(tblKind.KindCode.HostName, Name);
        }

        if (!string.IsNullOrWhiteSpace(TeamViewerId))
        {
            properties.Add(tblKind.KindCode.TeamViewerDeviceId, TeamViewerId);
        }

        if (LastCheckInUtc.HasValue)
        {
            properties.Add(tblKind.KindCode.TeamViewerLastCheckIn, LastCheckInUtc.Value);
        }

        if (!string.IsNullOrWhiteSpace(Manufacturer))
        {
            properties.Add(tblKind.KindCode.Manufacturer, Manufacturer);
        }

        if (RamCapacityInGb.HasValue)
        {
            properties.Add((tblKind.KindCode) KindCode.RAMCapacityInGB, RamCapacityInGb.Value);
        }

        if (TotalStorageCapacityInGb.HasValue)
        {
            properties.Add((tblKind.KindCode) KindCode.TotalStorageSpaceInGB, TotalStorageCapacityInGb.Value);
        }

        return properties;
    }
}

internal static class TeamViewerHardwareParser
{
    internal static void Apply(TeamViewerDevice device, JArray items)
    {
        var hardwareItems = new List&lt;JObject&gt;();

        foreach (JToken item in items)
        {
            JObject hardwareItem = item as JObject;

            if (hardwareItem != null)
            {
                hardwareItems.Add(hardwareItem);
            }
        }

        device.Manufacturer = GetHardwareManufacturer(hardwareItems, TeamViewerHardwareType.Motherboard) ?? GetHardwareManufacturer(hardwareItems, TeamViewerHardwareType.Bios);

        device.BiosSerialNumber = GetHardwareSerialNumber(hardwareItems, TeamViewerHardwareType.Bios) ?? GetHardwareSerialNumber(hardwareItems, TeamViewerHardwareType.Motherboard);

        device.RamCapacityInGb = SumHardwareCapacityInGb(hardwareItems, TeamViewerHardwareType.PhysicalMemory);

        device.TotalStorageCapacityInGb = SumHardwareCapacityInGb(hardwareItems, TeamViewerHardwareType.DiskDrive);
    }

    private static string GetHardwareManufacturer(IEnumerable&lt;JObject&gt; items, TeamViewerHardwareType hardwareType)
    {
        return items.Where(item =&gt; item.Value&lt;int?&gt;("type") == (int)hardwareType).Select(item =&gt; item.Value&lt;string&gt;("manufacturer")).FirstOrDefault(manufacturer =&gt; !string.IsNullOrWhiteSpace(manufacturer))?.Trim();
    }

    private static string GetHardwareSerialNumber(IEnumerable&lt;JObject&gt; items, TeamViewerHardwareType hardwareType)
    {
        foreach (JObject item in items)
        {
            if (item.Value&lt;int?&gt;("type") != (int)hardwareType)
            {
                continue;
            }

            string details = item.Value&lt;string&gt;("details");

            string serialNumber = ParseSerialNumber(details);

            if (!string.IsNullOrWhiteSpace(serialNumber))
            {
                return serialNumber;
            }
        }

        return null;
    }

    private static string ParseSerialNumber(string details)
    {
        if (string.IsNullOrWhiteSpace(details))
        {
            return null;
        }

        Match match = Regex.Match(details, @"(?:^|,\s*)Serial\s+number\s*:\s*(?&lt;value&gt;.+?)\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

        if (!match.Success)
        {
            return null;
        }

        string value = match.Groups["value"].Value.Trim();

        return string.IsNullOrWhiteSpace(value) ? null : value;
    }

    private static double? SumHardwareCapacityInGb(IEnumerable&lt;JObject&gt; items, TeamViewerHardwareType hardwareType)
    {
        List&lt;double&gt; capacities = items.Where(item =&gt; item.Value&lt;int?&gt;("type") == (int)hardwareType).Select(item =&gt; ParseCapacityInGb(item.Value&lt;string&gt;("details"))).Where(capacity =&gt; capacity.HasValue).Select(capacity =&gt; capacity.Value).ToList();

        if (capacities.Count == 0)
        {
            return null;
        }

        return Math.Round(capacities.Sum(), 2, MidpointRounding.AwayFromZero);
    }

    private static double? ParseCapacityInGb(string details)
    {
        if (string.IsNullOrWhiteSpace(details))
        {
            return null;
        }

        Match match = Regex.Match(details, @"^\s*(?&lt;value&gt;\d+(?:[.,]\d+)?)\s*" + @"(?&lt;unit&gt;TB|GB|MB|KB|B)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

        if (!match.Success)
        {
            return null;
        }

        string numericValue = match.Groups["value"].Value.Replace(",", ".");

        if (!double.TryParse(numericValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
        {
            return null;
        }

        string unit = match.Groups["unit"].Value.ToUpperInvariant();

        switch (unit)
        {
            case "TB":
                return value * 1024d;

            case "GB":
                return value;

            case "MB":
                return value / 1024d;

            case "KB":
                return value / 1024d / 1024d;

            case "B":
                return value
                    / 1024d
                    / 1024d
                    / 1024d;

            default:
                return null;
        }
    }
}

internal static class TeamViewerDeviceMerger
{
    internal static List&lt;TeamViewerDevice&gt; GetUniqueDevices(List&lt;TeamViewerDevice&gt; devices)
    {
        var result = new List&lt;TeamViewerDevice&gt;();

        foreach (TeamViewerDevice device in devices)
        {
            NormalizeDevice(device);

            TeamViewerDevice existingDevice = FindMatchingDevice(result, device);

            if (existingDevice == null)
            {
                EnsureSources(device);
                result.Add(device);
                continue;
            }

            MergeDevice(existingDevice, device);
        }

        return result;
    }

    internal static void NormalizeDevice(TeamViewerDevice device)
    {
        device.ManagementId = NormalizeIdentifier(device.ManagementId);

        device.ContactDeviceId = NormalizeIdentifier(device.ContactDeviceId);

        device.TeamViewerId = NormalizeTeamViewerId(device.TeamViewerId);

        device.Name = string.IsNullOrWhiteSpace(device.Name) ? null : device.Name.Trim();

        EnsureSources(device);
    }

    private static TeamViewerDevice FindMatchingDevice(List&lt;TeamViewerDevice&gt; devices, TeamViewerDevice device)
    {
        foreach (TeamViewerDevice candidate in devices)
        {
            if (!string.IsNullOrEmpty(device.TeamViewerId) &amp;&amp; string.Equals(candidate.TeamViewerId, device.TeamViewerId, StringComparison.OrdinalIgnoreCase))
            {
                return candidate;
            }

            if (!string.IsNullOrEmpty(device.ManagementId) &amp;&amp; string.Equals(candidate.ManagementId, device.ManagementId, StringComparison.OrdinalIgnoreCase))
            {
                return candidate;
            }

            if (!string.IsNullOrEmpty(device.ContactDeviceId) &amp;&amp; string.Equals(candidate.ContactDeviceId, device.ContactDeviceId, StringComparison.OrdinalIgnoreCase))
            {
                return candidate;
            }
        }

        return null;
    }

    private static void MergeDevice(TeamViewerDevice target, TeamViewerDevice source)
    {
        if (string.IsNullOrEmpty(target.ManagementId))
        {
            target.ManagementId = source.ManagementId;
        }

        if (string.IsNullOrEmpty(target.ContactDeviceId))
        {
            target.ContactDeviceId = source.ContactDeviceId;
        }

        if (string.IsNullOrEmpty(target.TeamViewerId))
        {
            target.TeamViewerId = source.TeamViewerId;
        }

        if (string.IsNullOrEmpty(target.Name))
        {
            target.Name = source.Name;
        }

        if (!target.OnlineState.HasValue || IsMoreCurrentOnlineState(source.OnlineState, target.OnlineState))
        {
            target.OnlineState = source.OnlineState;
        }

        EnsureSources(target);
        EnsureSources(source);

        foreach (string sourceName in source.Sources)
        {
            target.Sources.Add(sourceName);
        }
    }

    private static bool IsMoreCurrentOnlineState(TeamViewerOnlineState? source, TeamViewerOnlineState? target)
    {
        if (!source.HasValue)
        {
            return false;
        }

        if (!target.HasValue)
        {
            return true;
        }

        if (source.Value == TeamViewerOnlineState.Unknown)
        {
            return false;
        }

        return target.Value == TeamViewerOnlineState.Unknown;
    }

    private static void EnsureSources(TeamViewerDevice device)
    {
        if (device.Sources == null)
        {
            device.Sources = new HashSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase);
        }
    }

    internal static string NormalizeIdentifier(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return value.Replace(" ", string.Empty).Replace("\u00A0", string.Empty).Trim();
    }

    internal static string NormalizeTeamViewerId(string value)
    {
        string normalized = NormalizeIdentifier(value);

        if (string.IsNullOrEmpty(normalized))
        {
            return null;
        }

        return normalized.StartsWith("r", StringComparison.OrdinalIgnoreCase) ? normalized.Substring(1) : normalized;
    }
}

internal class MatchedTeamViewerDevice
{
    public TeamViewerDevice Device { get; set; }

    public tblNode Node { get; set; }
}

internal class TeamViewerImportCollisionChecker
{
    private readonly Dictionary&lt;int, CollisionInfo&gt; MatchMap = new Dictionary&lt;int, CollisionInfo&gt;();

    public void AddImportedInfo(int nodeId, TeamViewerDevice device, bool? errorSetBeforeImport = null)
    {
        bool isFirstOccurrence = !MatchMap.ContainsKey(nodeId);

        if (isFirstOccurrence)
        {
            MatchMap.Add(nodeId, new CollisionInfo());
        }

        CollisionInfo collisionInfo = MatchMap[nodeId];

        if (errorSetBeforeImport.HasValue &amp;&amp; isFirstOccurrence)
        {
            collisionInfo.PresentBeforeImport = errorSetBeforeImport.Value;
        }

        collisionInfo.CollidingObjects.Add(device);
    }

    public bool ShouldLogCollision(int nodeId)
    {
        return nodeId &gt; 0 &amp;&amp; HasCollision(nodeId) &amp;&amp; !HadCollisionBeforeImportStarted(nodeId);
    }

    public bool HasCollision(int nodeId)
    {
        CollisionInfo collisionInfo;

        if (!MatchMap.TryGetValue(nodeId, out collisionInfo))
        {
            return false;
        }

        return collisionInfo.CollidingObjects.Count &gt;= 2;
    }

    public bool HadCollisionBeforeImportStarted(int nodeId)
    {
        CollisionInfo collisionInfo;

        if (!MatchMap.TryGetValue(nodeId, out collisionInfo))
        {
            return false;
        }

        return collisionInfo.PresentBeforeImport;
    }

    public tblLog GetCollisionLog(int nodeId, int personId)
    {
        return new tblLog
        {
            lintNodeId = nodeId,
            liLogPersonId = personId,
            dteLog = DateTime.UtcNow,
            txtLog = GetCollisionLogMessage(nodeId)
        };
    }

    public Dictionary&lt;int, CollisionInfo&gt; GetCollidingRecords()
    {
        Dictionary&lt;int, CollisionInfo&gt; collisionsOnExistingObjects = MatchMap.Where(item =&gt; item.Key &gt; 0 &amp;&amp; item.Value.CollidingObjects.Count &gt;= 2).ToDictionary(item =&gt; item.Key, item =&gt; item.Value);

        if (!MatchMap.ContainsKey(0))
        {
            return collisionsOnExistingObjects;
        }

        List&lt;TeamViewerDevice&gt; unmatchedDevices = MatchMap[0].CollidingObjects;

        IEnumerable&lt;TeamViewerDevice&gt; collisionsByBiosSerialNumber = unmatchedDevices.Where(device =&gt; !string.IsNullOrWhiteSpace(device.BiosSerialNumber)).GroupBy(device =&gt; device.BiosSerialNumber.Trim(), StringComparer.OrdinalIgnoreCase).Where(group =&gt; group.Count() &gt;= 2).SelectMany(group =&gt; group);

        IEnumerable&lt;TeamViewerDevice&gt; collisionsByHostname = unmatchedDevices.Where(device =&gt; !string.IsNullOrWhiteSpace(device.Name)).GroupBy(device =&gt; device.Name.Trim(), StringComparer.OrdinalIgnoreCase).Where(group =&gt; group.Count() &gt;= 2).SelectMany(group =&gt; group);

        List&lt;TeamViewerDevice&gt; collisionsOnObjectsToCreate = collisionsByBiosSerialNumber.Concat(collisionsByHostname).Distinct().ToList();

        if (collisionsOnObjectsToCreate.Count &gt; 0)
        {
            collisionsOnExistingObjects.Add(0, new CollisionInfo { PresentBeforeImport = false, CollidingObjects = collisionsOnObjectsToCreate });
        }

        return collisionsOnExistingObjects;
    }

    public IEnumerable&lt;int&gt; GetNodesToUnsetCollisionFlag()
    {
        return MatchMap.Where(item =&gt; item.Key &gt; 0 &amp;&amp; item.Value.CollidingObjects.Count == 1 &amp;&amp; item.Value.PresentBeforeImport).Select(item =&gt; item.Key);
    }

    private string GetCollisionLogMessage(int nodeId)
    {
        CollisionInfo collisionInfo = MatchMap[nodeId];

        List&lt;string&gt; identifiers = collisionInfo.CollidingObjects.Select(device =&gt; !string.IsNullOrWhiteSpace(device.TeamViewerId) ? device.TeamViewerId : device.ManagementId).Where(identifier =&gt; !string.IsNullOrWhiteSpace(identifier)).Take(3).ToList();

        string threeDots = collisionInfo.CollidingObjects.Count &gt; 3 ? "..." : string.Empty;

        return string.Format(CultureInfo.InvariantCulture, "Duplicate devices were found during import. " + "TeamViewer device IDs: {0}{1}", string.Join(", ", identifiers), threeDots);
    }

    public class CollisionInfo
    {
        public bool PresentBeforeImport { get; set; }

        public List&lt;TeamViewerDevice&gt; CollidingObjects { get; set; }

        public CollisionInfo()
        {
            CollidingObjects = new List&lt;TeamViewerDevice&gt;();
        }
    }
}

internal class TeamViewerApiClient
{
    private static readonly HttpClient HttpClient = new HttpClient();

    internal async Task&lt;List&lt;TeamViewerDevice&gt;&gt; GetDevicesAsync(CancellationToken cancellationToken)
    {
        List&lt;TeamViewerDevice&gt; managedDevices = await GetManagedDevicesAsync(cancellationToken);

        List&lt;TeamViewerDevice&gt; computersAndContacts = await GetComputersAndContactsDevicesAsync(cancellationToken);

        var allDevices = new List&lt;TeamViewerDevice&gt;();
        allDevices.AddRange(managedDevices);
        allDevices.AddRange(computersAndContacts);

        return TeamViewerDeviceMerger.GetUniqueDevices(allDevices);
    }

    internal async Task LoadHardwareAsync(TeamViewerDevice device, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(device.TeamViewerId))
        {
            return;
        }

        string relativeUrl = "monitoring/devices/" + Uri.EscapeDataString(device.TeamViewerId) + "/hardware";

        JToken json = await GetJsonAsync(relativeUrl, "Hardware", cancellationToken);

        JObject root = json as JObject;

        if (root == null)
        {
            throw new InvalidOperationException("Unable to synchronize devices from Hardware. " + "The API returned an unexpected response.");
        }

        JArray items = root["items"] as JArray;

        if (items == null)
        {
            throw new InvalidOperationException("Unable to synchronize devices from Hardware. " + "The response does not contain the items array.");
        }

        TeamViewerHardwareParser.Apply(device, items);
    }

    private static TeamViewerDevice ParseManagedDevice(JObject record)
    {
    if (record == null)
    {
        return null;
    }

    return new TeamViewerDevice
    {
        ManagementId = record.Value&lt;string&gt;("id"),
        TeamViewerId = TeamViewerDeviceMerger.NormalizeTeamViewerId(record["teamviewerId"]?.ToString()),
        Name = record.Value&lt;string&gt;("name"),
        OnlineState = ParseManagedOnlineState(record["isOnline"]),
        Sources = new HashSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase)
        {
            "Managed devices"
        }
    };
    }

    private static TeamViewerOnlineState? ParseManagedOnlineState(JToken token)
    {
        if (token == null || token.Type == JTokenType.Null)
        {
            return null;
        }

        bool isOnline;

        if (!bool.TryParse(token.ToString(), out isOnline))
        {
            return null;
        }

        return isOnline
            ? TeamViewerOnlineState.Online : TeamViewerOnlineState.Offline;
    }

    private static TeamViewerDevice ParseComputersAndContactsDevice(JObject record)
    {
        if (record == null)
        {
            return null;
        }

        return new TeamViewerDevice
        {
            ContactDeviceId = TeamViewerDeviceMerger.NormalizeIdentifier(record.Value&lt;string&gt;("device_id")),
            TeamViewerId = TeamViewerDeviceMerger.NormalizeTeamViewerId(record["teamviewer_id"]?.ToString() ?? record["remotecontrol_id"]?.ToString()),
            Name = record.Value&lt;string&gt;("alias"),
            OnlineState = ParseOnlineState(record["online_state"]),
            Sources = new HashSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase)
            {
                "Computers and contacts"
            }
        };
    }

    private static TeamViewerOnlineState? ParseOnlineState(JToken token)
    {
        if (token == null || token.Type == JTokenType.Null)
        {
            return null;
        }

        int state;

        if (!int.TryParse(token.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out state))
        {
            return null;
        }

        if (!Enum.IsDefined(typeof(TeamViewerOnlineState), state))
        {
            return null;
        }

        return (TeamViewerOnlineState)state;
    }

    private async Task&lt;JToken&gt; GetJsonAsync(string relativeUrl, string endpointName, CancellationToken cancellationToken)
    {
        string url = TeamViewerConnectorCommon.ApiBaseUrl + relativeUrl;

        using var request = new HttpRequestMessage(HttpMethod.Get, url);

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", DbProperty.TeamViewerToken);

        using HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken);

        string responseContent = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            string errorMessage = GetApiErrorMessage(responseContent, response.ReasonPhrase);

            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to synchronize devices from {0}. " + "Error message: {1} - {2}", endpointName, (int)response.StatusCode, errorMessage));
        }

        if (string.IsNullOrWhiteSpace(responseContent))
        {
            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to synchronize devices from {0}. " + "Error message: The API returned an empty response.", endpointName));
        }

        try
        {
            return JToken.Parse(responseContent);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to synchronize devices from {0}. " + "Error message: The API returned invalid JSON.", endpointName), exception);
        }
    }

    private static string GetApiErrorMessage(string responseContent, string reasonPhrase)
    {
        if (!string.IsNullOrWhiteSpace(responseContent))
        {
            try
            {
                JObject errorResponse = JObject.Parse(responseContent);

                string errorDescription = errorResponse.Value&lt;string&gt;("error_description");

                if (!string.IsNullOrWhiteSpace(errorDescription))
                {
                    return errorDescription.Trim();
                }

                string error = errorResponse.Value&lt;string&gt;("error");

                if (!string.IsNullOrWhiteSpace(error))
                {
                    return error.Trim();
                }
            }
            catch
            {
                // The response is not a TeamViewer JSON error.
            }
        }

        if (!string.IsNullOrWhiteSpace(reasonPhrase))
        {
            return reasonPhrase.Trim();
        }

        return "The request failed.";
    }

    private async Task&lt;List&lt;TeamViewerDevice&gt;&gt; GetManagedDevicesAsync(CancellationToken cancellationToken)
    {
        var result = new List&lt;TeamViewerDevice&gt;();

        var processedPaginationTokens = new HashSet&lt;string&gt;(StringComparer.Ordinal);

        string paginationToken = null;

        do
        {
            if (!string.IsNullOrWhiteSpace(paginationToken) &amp;&amp; !processedPaginationTokens.Add(paginationToken))
            {
                throw new InvalidOperationException("Unable to synchronize devices from Managed devices. " + "The API returned a repeated pagination token.");
            }

            string relativeUrl = "managed/devices";

            if (!string.IsNullOrWhiteSpace(paginationToken))
            {
                relativeUrl += "?paginationToken=" + Uri.EscapeDataString(paginationToken);
            }

            JToken json = await GetJsonAsync(relativeUrl, "Managed devices", cancellationToken);

            JObject root = json as JObject;

            if (root == null)
            {
                throw new InvalidOperationException("Unable to synchronize devices from Managed devices. " + "The API returned an unexpected response.");
            }

            JArray resources = root["resources"] as JArray;

            if (resources == null)
            {
                throw new InvalidOperationException("Unable to synchronize devices from Managed devices. " + "The response does not contain the resources array.");
            }

            for (int index = 0; index &lt; resources.Count; index++)
            {
                JObject record = resources[index] as JObject;

                if (record == null)
                {
                    TeamViewerConnectorCommon.Log.Warn("TeamViewer Connector: Managed devices response " + "contains an invalid record at index {0}.", index);

                    continue;
                }

                TeamViewerDevice device = ParseManagedDevice(record);

                if (device != null)
                {
                    result.Add(device);
                }
            }

            paginationToken = root.Value&lt;string&gt;("nextPaginationToken");

        } while (!string.IsNullOrWhiteSpace(paginationToken));

        return result;
    }

    private async Task&lt;List&lt;TeamViewerDevice&gt;&gt; GetComputersAndContactsDevicesAsync(CancellationToken cancellationToken)
    {
        JToken json = await GetJsonAsync("devices?full_list=true", "Computers and contacts", cancellationToken);

        JObject root = json as JObject;

        if (root == null)
        {
            throw new InvalidOperationException("Unable to synchronize devices from Computers and contacts. " + "The API returned an unexpected response.");
        }

        JArray devices = root["devices"] as JArray;

        if (devices == null)
        {
            throw new InvalidOperationException("Unable to synchronize devices from Computers and contacts. " + "The response does not contain the devices array.");
        }

        var result = new List&lt;TeamViewerDevice&gt;();

        for (int index = 0; index &lt; devices.Count; index++)
        {
            JObject record = devices[index] as JObject;
            TeamViewerDevice device = ParseComputersAndContactsDevice(record);

            if (device != null)
            {
                result.Add(device);
            }
        }

        return result;
    }
}

internal class TeamViewerAssetService
{
    private readonly TeamViewerConnectorRepository Repository;
    private readonly Dictionary&lt;int, bool&gt; CheckedTemplatePropertyMap = new Dictionary&lt;int, bool&gt;();
    private int ImportedObjectsNodeFolderNodeId;

    internal TeamViewerAssetService(TeamViewerConnectorRepository repository)
    {
        Repository = repository;
    }

    internal tblNode GetExistingDeviceNode(TeamViewerDevice device)
    {
        tblNode node = null;

        if (!string.IsNullOrWhiteSpace(device.ManagementId))
        {
            node = Repository.GetDeviceNodeByProperty(tblKind.KindCode.TeamViewerGuid, device.ManagementId, false);
        }

        if (node == null &amp;&amp; !string.IsNullOrWhiteSpace(device.Name))
        {
            node = Repository.GetDeviceNodeByProperty(tblKind.KindCode.HostName, device.Name, true);
        }

        if (node == null &amp;&amp; !string.IsNullOrWhiteSpace(device.TeamViewerId))
        {
            node = Repository.GetDeviceNodeByProperty(tblKind.KindCode.TeamViewerDeviceId, device.TeamViewerId, false);
        }

        return node;
    }

    internal void EnsureTemplateProperties(int classId)
    {
        if (CheckedTemplatePropertyMap.ContainsKey(classId))
        {
            return;
        }

        string kindCodes = string.Join(",", new[] { ((int)KindCode.TeamViewerGuid).ToString(CultureInfo.InvariantCulture), ((int)KindCode.HostName).ToString(CultureInfo.InvariantCulture), ((int)KindCode.TeamViewerDeviceId).ToString(CultureInfo.InvariantCulture), ((int)KindCode.TeamViewerLastCheckIn).ToString(CultureInfo.InvariantCulture), ((int)KindCode.Manufacturer).ToString(CultureInfo.InvariantCulture), ((int)KindCode.RAMCapacityInGB).ToString(CultureInfo.InvariantCulture), ((int)KindCode.TotalStorageSpaceInGB).ToString(CultureInfo.InvariantCulture) });

        Repository.AddMissingPropertiesToAmTemplate(classId, kindCodes);

        CheckedTemplatePropertyMap[classId] = true;
    }

    private static bool IsComputer(int classId)
    {
        using var scope = AlvaoContext.GetConnectionScope();

        return scope.Connection.ExecuteScalar&lt;bool?&gt;(@"
            select bComputer
            from tblClass
            where intClassId = @classId;
            ", new { classId }, transaction: scope.Transaction) ?? false;
    }

    internal tblNode CreateNewDevice(TeamViewerDevice device, int systemPersonId)
    {
        if (string.IsNullOrWhiteSpace(device.Name))
        {
            throw new InvalidOperationException("A TeamViewer device without a name cannot be created.");
        }

        if (ImportedObjectsNodeFolderNodeId == 0)
        {
            ImportedObjectsNodeFolderNodeId = Repository.GetOrCreateImportedObjectsFolder();
        }

        int classId = Alvao.API.AM.ObjectType.GetDeviceTypeId(device.Name, null, 0);

        if (classId &lt;= 0)
        {
            classId = Alvao.API.Common.DbProperty.AMDefaultComputerClass;
        }

        if (classId &lt;= 0)
        {
            throw new InvalidOperationException("The default AM computer class is not configured.");
        }

        EnsureTemplateProperties(classId);

        if (IsComputer(classId))
        {
            return Repository.CreateSimpleComputer(classId, ImportedObjectsNodeFolderNodeId, device.Name, systemPersonId);
        }

        return Repository.CreateObjectByClass(classId, ImportedObjectsNodeFolderNodeId, systemPersonId);
    }

    internal List&lt;(tblKind.KindCode KindCode, object Value)&gt; UpdateDeviceProperties(int nodeId, TeamViewerDevice device)
    {
        var updatedProperties = new List&lt;(tblKind.KindCode KindCode, object Value)&gt;();

        Dictionary&lt;tblKind.KindCode, object&gt; propertyMapping = device.GetPropertyMapping();

        foreach (KeyValuePair&lt;tblKind.KindCode, object&gt; property in propertyMapping)
        {
            if (property.Value == null)
            {
                continue;
            }

            object valueToStore;

            bool isNumericProperty = property.Key == (tblKind.KindCode)KindCode.RAMCapacityInGB || property.Key == (tblKind.KindCode)KindCode.TotalStorageSpaceInGB;

            if (isNumericProperty)
            {
                valueToStore = Convert.ToDouble(property.Value, CultureInfo.InvariantCulture);
            }
            else if (property.Value is DateTime dateTimeValue)
            {
                valueToStore = dateTimeValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
            }
            else
            {
                valueToStore = Convert.ToString(property.Value, CultureInfo.InvariantCulture);
            }


            if (valueToStore is string stringValue &amp;&amp; string.IsNullOrWhiteSpace(stringValue))
            {
                continue;
            }

            bool shouldLog = property.Key != tblKind.KindCode.TeamViewerLastCheckIn;

            Alvao.API.AM.ObjectProperty.Update(nodeId, property.Key, valueToStore, false, shouldLog);

            updatedProperties.Add((property.Key, valueToStore));
        }

        return updatedProperties;
    }

    internal void NotifyPropertyChanges(int nodeId, int systemPersonId, List&lt;(tblKind.KindCode KindCode, object Value)&gt; updatedProperties)
    {
    foreach ((tblKind.KindCode kindCode, object value) in updatedProperties)
        {
            try
            {
                int? kindId = Repository.GetKindId(kindCode);

                if (!kindId.HasValue)
                {
                    TeamViewerConnectorCommon.Log.Warn("TeamViewer Connector: Unable to notify " + "property change for KindCode {0}. " + "The property definition was not found.", (int)kindCode);

                    continue;
                }

                var request = new OnObjectPropertyModifiedRequest(nodeId, kindId.Value, systemPersonId, value);

                Alvao.API.AM.CustomApps.OnObjectPropertyModified(request);
            }
            catch (Exception exception)
            {
                TeamViewerConnectorCommon.Log.Warn(exception, "TeamViewer Connector: Unable to notify " + "property change for KindCode {0} " + "on object {1}.", (int)kindCode, nodeId);
            }
        }
    }
}

internal class TeamViewerCollisionService
{
    private readonly TeamViewerConnectorRepository Repository;
    private readonly TeamViewerAssetService AssetService;

    internal TeamViewerCollisionService(TeamViewerConnectorRepository repository, TeamViewerAssetService assetService)
    {
        Repository = repository;
        AssetService = assetService;
    }

    private void SetAndLogCollisions(TeamViewerImportCollisionChecker collisionChecker, int systemPersonId)
    {
        Dictionary&lt;int, TeamViewerImportCollisionChecker.CollisionInfo&gt; collisions = collisionChecker.GetCollidingRecords();

        foreach (KeyValuePair&lt; int, TeamViewerImportCollisionChecker.CollisionInfo&gt; collision in collisions)
        {
            if (collisionChecker.ShouldLogCollision(collision.Key))
            {
                tblLog log = collisionChecker.GetCollisionLog(collision.Key, systemPersonId);

                using var scope = AlvaoContext.GetConnectionScope();

                scope.BeginTransaction();

                Repository.InsertIntoObjectLog(log);

                Repository.SetImportConflict(collision.Key, true);

                scope.CommitTransaction();
            }
        }

        IEnumerable&lt;int&gt; nodesToUnset = collisionChecker.GetNodesToUnsetCollisionFlag();

        foreach (int nodeId in nodesToUnset)
        {
            Repository.SetImportConflict(nodeId, false);
        }
    }

    internal List&lt;MatchedTeamViewerDevice&gt; MatchDevicesAndProcessCollisions(List&lt;TeamViewerDevice&gt; devices, int systemPersonId, CancellationToken cancellationToken, out int collisionCount)
    {
        var collisionChecker = new TeamViewerImportCollisionChecker();

        var matchedDevices = new List&lt;MatchedTeamViewerDevice&gt;();

        foreach (TeamViewerDevice device in devices)
        {
            cancellationToken.ThrowIfCancellationRequested();

            tblNode node = AssetService.GetExistingDeviceNode(device);

            if (node != null)
            {
                device.NodeId = node.intNodeId;

                collisionChecker.AddImportedInfo(node.intNodeId, device, node.ImportDuplicateAlert);
            }
            else
            {
                collisionChecker.AddImportedInfo(0, device);
            }

            matchedDevices.Add(new MatchedTeamViewerDevice { Device = device, Node = node });
        }

        SetAndLogCollisions(collisionChecker, systemPersonId);

        Dictionary&lt;int, TeamViewerImportCollisionChecker.CollisionInfo&gt; collisions = collisionChecker.GetCollidingRecords();

        var collidingDevices = new HashSet&lt;TeamViewerDevice&gt;();

        foreach (TeamViewerImportCollisionChecker.CollisionInfo collision in collisions.Values)
        {
            foreach (TeamViewerDevice device in collision.CollidingObjects)
            {
                collidingDevices.Add(device);
            }
        }

        collisionCount = collidingDevices.Count;

        return matchedDevices.Where(item =&gt; !collidingDevices.Contains(item.Device)).ToList();
    }
}

internal static class TeamViewerText
{
    internal static string Truncate(string value, int maximumLength)
    {
        if (string.IsNullOrEmpty(value))
        {
            return string.Empty;
        }

        return value.Length &lt;= maximumLength ? value : value.Substring(0, maximumLength) + "...";
    }
}

internal class TeamViewerImportService
{
    private readonly TeamViewerApiClient ApiClient;
    private readonly TeamViewerConnectorRepository Repository;
    private readonly TeamViewerAssetService AssetService;
    private readonly TeamViewerCollisionService CollisionService;

    internal TeamViewerImportService()
    {
        Repository = new TeamViewerConnectorRepository();
        ApiClient = new TeamViewerApiClient();
        AssetService = new TeamViewerAssetService(Repository);
        CollisionService = new TeamViewerCollisionService(Repository, AssetService);
    }

    internal async Task RunAsync(CancellationToken cancellationToken)
    {
        List&lt;TeamViewerDevice&gt; devices = await ApiClient.GetDevicesAsync(cancellationToken);

        DateTime importDateUtc = DateTime.UtcNow;

        foreach (TeamViewerDevice device in devices)
        {
            cancellationToken.ThrowIfCancellationRequested();

            device.LastCheckInUtc = importDateUtc;

            if (string.IsNullOrWhiteSpace(device.TeamViewerId))
            {
                continue;
            }

            await LoadHardwareSafelyAsync(device, cancellationToken);
        }

        TeamViewerConnectorCommon.Log.Info("TeamViewer Connector: Loaded {0} TeamViewer devices.", devices.Count);

        int matchedCount = 0;
        int createdCount = 0;
        int collisionCount;
        int errorCount = 0;
        int systemPersonId = Alvao.API.Common.Person.GetSystem().iPersonId;

        Repository.ClearScannerPropertyLockoutTable();

        List&lt;MatchedTeamViewerDevice&gt; matchedDevices = CollisionService.MatchDevicesAndProcessCollisions(devices, systemPersonId, cancellationToken, out collisionCount);

        foreach (MatchedTeamViewerDevice matchedDevice in matchedDevices)
        {
            cancellationToken.ThrowIfCancellationRequested();

            TeamViewerDevice device = matchedDevice.Device;
            tblNode node = matchedDevice.Node;
            bool wasCreated = false;

            try
            {
                if (node == null)
                {
                    node = AssetService.CreateNewDevice(device, systemPersonId);

                    if (node == null || node.intNodeId &lt;= 0)
                    {
                        throw new InvalidOperationException("Creation of the ALVAO object " + "returned no object ID.");
                    }

                    wasCreated = true;
                }

                if (node.lintClassId &lt;= 0)
                {
                    throw new InvalidOperationException("The ALVAO object has no valid object type.");
                }

                device.NodeId = node.intNodeId;

                using var scope = AlvaoContext.GetConnectionScope();

                scope.BeginTransaction();

                AssetService.EnsureTemplateProperties(node.lintClassId);

                Repository.RecalculateScannerPropertyLockoutTable(node.lintClassId);

                List&lt;(tblKind.KindCode KindCode, object Value)&gt; updatedProperties = AssetService.UpdateDeviceProperties(node.intNodeId, device);

                scope.CommitTransaction();

                AssetService.NotifyPropertyChanges(node.intNodeId, systemPersonId, updatedProperties);

                if (wasCreated)
                {
                    createdCount++;

                    TeamViewerConnectorCommon.Log.Info("TeamViewer Connector: Created object '{0}'.", TeamViewerText.Truncate(device.Name, 100));
                }
                else
                {
                    matchedCount++;
                }
            }
            catch (Exception exception)
            {
                errorCount++;

                TeamViewerConnectorCommon.Log.Error(exception, "TeamViewer Connector: Error processing device '{0}'.", TeamViewerText.Truncate(device.Name, 100));
            }
        }

        TeamViewerConnectorCommon.Log.Info("TeamViewer Connector: Processed {0} devices. " + "Matched: {1}, created: {2}, " + "collisions: {3}, errors: {4}.", devices.Count, matchedCount, createdCount, collisionCount, errorCount);
    }

    private async Task LoadHardwareSafelyAsync(TeamViewerDevice device, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(device.TeamViewerId))
        {
            return;
        }

        try
        {
            await ApiClient.LoadHardwareAsync(device, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch
        {
            TeamViewerConnectorCommon.Log.Warn("TeamViewer Connector: Hardware data for device '{0}' " + "was not updated in this import cycle.", TeamViewerText.Truncate(device.Name, 100));
        }
    }
}

public class TeamViewerConnectorPeriodicAction : IPeriodicAction
{
    private readonly TeamViewerImportService ImportService = new TeamViewerImportService();

    public string Name
    {
        get =&gt; "TeamViewerConnectorPeriodicAction";
        set { }
    }

    public void OnPeriod(Microsoft.Data.SqlClient.SqlConnection connection)
    {
        if (!Activation.IsModuleActivated(ModuleId.RemoteAccessAndSupportConnectors))
        {
            TeamViewerConnectorCommon.Log.Warn("TeamViewer Connector: The connector cannot be used. " + "Module Remote Access and Support Connectors is not activated.");
            return;
        }

        if (string.IsNullOrWhiteSpace(DbProperty.TeamViewerToken))
        {
            TeamViewerConnectorCommon.Log.Warn("TeamViewer Connector: The connector is not properly configured. " + "Check TeamViewer.Token in advanced settings.");
            return;
        }

        try
        {
            ImportService.RunAsync(CancellationToken.None).GetAwaiter().GetResult();
        }
        catch (Exception exception)
        {
            TeamViewerConnectorCommon.Log.Error(exception, "TeamViewer Connector: {0}", exception.Message);
        }
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>r8xP6fsNU/Sgu2FAeafSH9clCvkG6JGc/PzN4jqdRcvWT6PqCA440dxUg9qrWWVxCY4v/UZ5z4mKdI68yqYImOunQaeiVdSHFi7gVfw1I5INeOXfdz0uiLrRkGcSpe9L/2sfLYf0M6BgZ9id3zoYD3NydOFgncZuOp+GQc9qhY6SWAN7B8NgQAC3CXtI/fM/VCkHoG+WCrrsoWZgYiWcczzVNN46gooRHsXrrPvgzBZQRU7lwLWkFqBlMbWGyiqsWkySRFSMbpzwCbrhXb7TBXlzgL55w0oDdCjOuZEbb4JmJgNQZ3Xub/tT3cQyvZ437XWZWtRlUfQ3Bt8Z73q2sg==</Codesign>
        </Script>
      </Scripts>
    </Application>
  </Applications>
</AlvaoApplication>