<?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="45">
      <Name>Splashtop Connector</Name>
      <UniqueId>afe0e56c-2b9d-4d6d-bf9a-157d19c8fafa</UniqueId>
      <Version>5</Version>
      <AdvancedSettings>
        <Setting>
          <Name>Splashtop.Token</Name>
          <Value />
        </Setting>
      </AdvancedSettings>
      <Scripts>
        <Script id="144">
          <Name>SplashtopConnectorPeriodicAction</Name>
          <Code>using System;
using System.Data;
using Microsoft.Data.SqlClient;
using Alvao.API.Common;
using Alvao.Apps.API;
using Alvao.API.Common.Model.Database;
using System.Threading;
using System.Collections.Generic;
using Alvao.Context;
using Dapper;
using System.Threading.Tasks;
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Linq;
using static Alvao.API.AM.Model.Kind;
using Alvao.API.Utils;
using static Alvao.Global.ModuleInfo;
using Alvao.API.Common.Model.CustomApps.Requests;
using Alvao.API.AM.Model;

namespace Alvao.CustomApps.SplashtopConnector;

public class PeriodicAction : IPeriodicAction
{
    public const string ConnectorName = "Splashtop";

    public string Name
    {
        get =&gt; "PeriodicAction";
        set { }
    }

    private readonly SplashtopConnectorRepository SplashtopConnectorRepository = new SplashtopConnectorRepository();
    private readonly SplashtopClientService SplashtopClientService = new SplashtopClientService();
    private int ImportedObjectsNodeFolderNodeId = 0;
    private Dictionary&lt;int, bool&gt; CheckedTemplatePropMap = new();

    public void OnPeriod(SqlConnection con)
    {
        if (!Activation.IsModuleActivated(ModuleId.RemoteAccessAndSupportConnectors))
        {
             Logger.Log.Warn($"{ConnectorName} Connector cannot be used. Module Remote Access and Support Connectors is not activated.");
             return;
        }
        
        if(string.IsNullOrEmpty(DbProperty.SplashtopToken))
        {
            Logger.Log.Warn($"{ConnectorName} Connector is not properly set. Check Splashtop.Token in advanced settings.");
            return;
        }
        try
        {
            Task task = Task.Run(async () =&gt; await CreateNewOrUpdateChangedDevices(CancellationToken.None));
            task.Wait();
        }
        catch (Exception ex)
        {
            Logger.Log.Error(ex, $"{ConnectorName} Connector: Error importing devices.");
        }
    }

    private async Task CreateNewOrUpdateChangedDevices(CancellationToken cancellationToken)
    {
        var systemPersonId = Alvao.API.Common.Person.GetSystem().iPersonId;

        IEnumerable&lt;SplashtopDevice&gt; newOrUpdatedIntuneDevices = await GetNewOrUpdatedDevices(cancellationToken);
        Logger.Log.Info($"Loaded {newOrUpdatedIntuneDevices.Count()} devices from Splashtop");

        cancellationToken.ThrowIfCancellationRequested();

        if (newOrUpdatedIntuneDevices is null || !newOrUpdatedIntuneDevices.Any())
            return;

        SplashtopConnectorRepository.CreateConnectorScannerIfNotExists();
        SplashtopConnectorRepository.ClearScannerPropertyLockoutTable();

        var collisionChecker = new ImportCollisionChecker();
        foreach (var device in newOrUpdatedIntuneDevices)
        {
            cancellationToken.ThrowIfCancellationRequested();

            using var scope = AlvaoContext.GetConnectionScope();
            try
            {
                scope.BeginTransaction();
                tblNode node;
                if (device.NodeId is null || device.NodeId == 0)
                {
                    node = CreateNewDevice(device);
                    if (node.intNodeId == 0)
                        continue;

                    Logger.Log.Info("{0} Connector: Created '{1}': {2}", ConnectorName, device.DeviceType.ToString(), device.Hostname);
                }
                else
                {
                    CheckAndAddPropertiesToTemplateByHostname(device.Hostname);
                    node = new tblNode() { intNodeId = device.NodeId.Value };
                }
                if (node is null)
                    throw new NullReferenceException($"Error: Device ID: '{device.SplashtopDeviceId}' not found in DB and creation failed.");

                SplashtopConnectorRepository.RecalculateScannerPropertyLockoutTable(device, node.lintClassId);

                List&lt;(tblKind.KindCode kindCode, object value)&gt; updatedProperties = UpdateDeviceProperties(node.intNodeId, device);
                scope.CommitTransaction();

                foreach(var property in updatedProperties){
                    OnObjectPropertyModifiedRequest req = new OnObjectPropertyModifiedRequest(node.intNodeId, Alvao.API.AM.ObjectProperty.GetDefinition(property.kindCode).intKindId, Alvao.API.Common.Person.GetSystem().iPersonId, property.value);
                    Alvao.API.AM.CustomApps.OnObjectPropertyModified(req);
                }
            }
            catch (NullReferenceException ex)
            {
                Logger.Log.Error(ex, $"{ConnectorName} Connector: Error processing device.");
            }
        }
        Logger.Log.Info("{0} Connector: Processed {1} devices", ConnectorName, newOrUpdatedIntuneDevices?.Count());
    }

    public tblNode GetExistingDeviceNode(SplashtopDevice device)
    {
        if (device.SplashtopDeviceId is null)
            throw new ArgumentNullException("Missing Splashtop ID.");

        tblNode node = SplashtopConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.SplashtopDeviceId, device.SplashtopDeviceId.ToString(), false);

        if (node == null)
        {
            var isSnBlacklisted = SplashtopConnectorRepository.GetBlacklistedValuesForKindcode((int)KindCode.BIOS_SN).Any(blacklisted =&gt; device.SerialNo == blacklisted);
            if (!isSnBlacklisted)
            {
                node = SplashtopConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.BIOS_SN, device.BIOS_SN, true);
            }
            if (node is null)
            {
                node = SplashtopConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.HostName, device.Hostname, true, string.IsNullOrEmpty(device.BIOS_SN));
            }
            if (node is null)
            {
                node = SplashtopConnectorRepository.GetDeviceNodeByAnotherProperty((int)KindCode.SerialNo, device.SerialNo, true, string.IsNullOrEmpty(device.BIOS_SN));
            }
        }
        return node;
    }

    private tblNode CreateNewDevice(SplashtopDevice managedDevice)
    {
        if (ImportedObjectsNodeFolderNodeId == 0)
            ImportedObjectsNodeFolderNodeId = SplashtopConnectorRepository.GetOrCreateImportedObjectsFolder();

        int classId = CheckAndAddPropertiesToTemplateByHostname(managedDevice.Hostname);

        tblNode node = new();
        if(IsComputer(classId))
            node = SplashtopConnectorRepository.CreateSimpleComputer(classId, ImportedObjectsNodeFolderNodeId, managedDevice.Hostname, Alvao.API.Common.Person.GetSystem().iPersonId);
        else
            node = SplashtopConnectorRepository.CreateObjectByClass(classId, ImportedObjectsNodeFolderNodeId, Alvao.API.Common.Person.GetSystem().iPersonId);

        return node;
    }

    private 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 }, scope.Transaction) ?? false;
    }

    private List&lt;(tblKind.KindCode kindCode, object value)&gt; UpdateDeviceProperties(int nodeId, SplashtopDevice device)
    {
        List&lt;(tblKind.KindCode kindCode, object value)&gt; codeValue = new List&lt;(tblKind.KindCode kindCode, object value)&gt;();
        using (var scope = AlvaoContext.GetConnectionScope())
        {
            scope.BeginTransaction();
            foreach (var mapping in device.GetPropertyMapping())
            {
                object value = typeof(SplashtopDevice).GetProperty(mapping.Value)?.GetValue(device);

                if(value is null)
                    continue;

                string strValue = value.ToString();
                if (mapping.Key.Equals(KindCode.RAMCapacityInGB) || mapping.Key.Equals(KindCode.TotalStorageSpaceInGB))
                {
                    Alvao.API.AM.ObjectProperty.Update(nodeId, (tblKind.KindCode)mapping.Key, Convert.ToDouble(value), false, true);    
                } else
                {
                Alvao.API.AM.ObjectProperty.Update(nodeId, (tblKind.KindCode)mapping.Key, strValue, false, true);
                }
                codeValue.Add(((tblKind.KindCode)mapping.Key, strValue));
            }
            scope.CommitTransaction();
        }
        return codeValue;
    }

    private async Task&lt;IEnumerable&lt;SplashtopDevice&gt;&gt; GetNewOrUpdatedDevices(CancellationToken cancellationToken) {
        var devices = await SplashtopClientService.GetNewOrUpdatedDevices(cancellationToken);
        var collisionChecker = new ImportCollisionChecker();
        foreach(var device in devices) 
        {
            tblNode node = GetExistingDeviceNode(device);
            if (node is null)
            {
                collisionChecker.AddImportedInfo(0, device);
            }
            else
            {
                device.NodeId = node.intNodeId;
                collisionChecker.AddImportedInfo(node.intNodeId, device, node.ImportDuplicateAlert);
            }
        }
        SetAndLogCollisions(collisionChecker);
        var toRemove = collisionChecker.GetCollidingRecords().SelectMany(c =&gt; c.Value.CollidingObjects);
        devices.ToList().RemoveAll(d =&gt; toRemove.Any(r =&gt; d.SplashtopDeviceId == r.SplashtopDeviceId));
        return devices;
    }

    private void SetAndLogCollisions(ImportCollisionChecker collisionChecker)
    {
        var collisions = collisionChecker.GetCollidingRecords();
        var systemPersonId = Alvao.API.Common.Person.GetSystem().iPersonId;
        foreach (var collision in collisions)
        {
            if (collisionChecker.ShouldLogCollision(collision.Key))
            {
                var log = collisionChecker.GetCollisionLog(collision.Key, systemPersonId);
                using var scope = AlvaoContext.GetConnectionScope();
                scope.BeginTransaction();
                SplashtopConnectorRepository.InsertIntoObjectLog(log);
                SplashtopConnectorRepository.SetImportConflict(collision.Key, true);
                scope.CommitTransaction();
            }
        }

        var noMoreColliding = collisionChecker.GetNodesToUnsetCollisionFlag();
        foreach (var nodeId in noMoreColliding)
        {
            SplashtopConnectorRepository.SetImportConflict(nodeId, false);
        }
    }
    private int CheckAndAddPropertiesToTemplateByHostname(string hostname)
    {
        int classId = Alvao.API.AM.ObjectType.GetDeviceTypeId(hostname, null, 0);
        if (classId == 0)
            classId = Alvao.API.Common.DbProperty.AMDefaultComputerClass;

        if (CheckedTemplatePropMap.TryAdd(classId, true))
        {
            if (!Alvao.API.AM.ObjectProperty.TemplateContains((int)classId, (tblKind.KindCode)KindCode.SplashtopDeviceId))
            {
                string kindCodes = string.Join(",", SplashtopDevice.GetPropertyMappingForType(SplashtopDevice.Type.Computer).Select(x =&gt; ((int)x.Key).ToString()));
                SplashtopConnectorRepository.AddMissingPropsToAmTemplateAndUnify((int)classId, kindCodes);
}
        }
        return classId;
    }
}

public class SplashtopConnectorRepository
{
    private List&lt;int&gt; ScannerPropMapCache = new();

    public tblNode GetDeviceNodeByAnotherProperty(int keyPropKindCode, string keyPropValue, bool onlyInComputers, bool isBiosSNInSourceEmpty = false)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.Query&lt;tblNode&gt;(@$"
            declare @columnName nvarchar(255)
            set @columnName = (select k.ColumnName from tblKind k where k.intKindCode = @kindCode)

            declare @valueType nvarchar(255)
            set @valueType = (select DATA_TYPE from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 'NodeCust' and COLUMN_NAME = @columnName)
            if (@valueType = 'varchar' or @valueType = 'nvarchar')
                set @valueType = @valueType + '(' + cast((select CHARACTER_MAXIMUM_LENGTH from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 'NodeCust' and COLUMN_NAME = @columnName) as nvarchar(255)) + ')'


            declare @sql nvarchar(max)
            set @sql = '
            select top 1
                n.intNodeId, 
                n.lintClassId,
                n.ImportDuplicateAlert
            from tblNode n
                {(onlyInComputers ? "join tblClass c on c.intClassId=n.lintClassId and c.bComputer=1" : "")}
                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(nc.' + @columnName + '))=@value 
                {(keyPropKindCode == (int)KindCode.HostName &amp;&amp; !isBiosSNInSourceEmpty ? $@"
                    and (nc.BiosSerialNumber is null or nc.BiosSerialNumber in {CreateBlacklistedForDynamicSql(GetCachedBlacklistedValuesForKindcode((int)KindCode.BIOS_SN))}) " : "")}
            order by n.intNodeId asc'

            declare @prmsDecl nvarchar(max)
            set @prmsDecl = N'@kindCode int, @value ' + @valueType

            exec sp_executesql @sql, @prmsDecl, @kindCode, @value",
            new { kindCode = keyPropKindCode, value = (keyPropValue != null) ? keyPropValue.Trim() : null },
            transaction: scope.Transaction).FirstOrDefault();
    }

    private string CreateBlacklistedForDynamicSql(IEnumerable&lt;string&gt; blacklisted)
    {
        StringBuilder sb = new();
        sb.Append('(').Append(string.Join(',', blacklisted.Select(it =&gt; "''" + it + "''"))).Append(')');
        return sb.ToString();
    }

    public IEnumerable&lt;string&gt; GetCachedBlacklistedValuesForKindcode(int kindCode)
    {
        return CacheUtil.GetOrCreateCachedItem("blacklistedValuesForKindCode_" + kindCode, () =&gt; GetBlacklistedValuesForKindcode(kindCode), minutesToCache: 30);
    }

    public IEnumerable&lt;string&gt; GetBlacklistedValuesForKindcode(int kindCode)
    {
        var wbemProp = ObjectWbemProcess.GetWbemEquivalentNameAndClass(kindCode);
        if (wbemProp is null)
        {
            return Enumerable.Empty&lt;string&gt;();
        }
        using (var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.Query&lt;string&gt;(
                @$"select 
                    txtPropValue
                from tblWbemObjectProcess
                where txtPropName = '{wbemProp.Name}'
                    and txtCLASS = '{wbemProp.Class}'
                ", new { }, scope.Transaction);
        }
    }

    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 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 tblNodeParent 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 int GetActualComputerCount()
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.ExecuteScalar&lt;int&gt;(@"select count(1) cnt 
        from tblNode n
        join tblClass c on c.intClassId=n.lintClassId
        where c.bComputer=1
        and n.IsActive = 1", 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, @setNodeId int
exec Internal.spCreateSimpleComputer @hostname, @parentNodeId, @personId, @classId, 1, @pcNodeId output, @setNodeId output
select @pcNodeId intNodeId, @classId lintClassId",
            new { hostname, parentNodeId, personId, classId }, transaction: scope.Transaction);
    }

    public tblNode CreateObjectByClass(int classId, int importedObjectsNodeFolderNodeId, int personId)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        return scope.Connection.QueryFirst&lt;tblNode&gt;(@"declare @id int
exec @id=spCreateNodeFromTemplate @classId, '', @parentId, @personId
select @id intNodeId, @classId lintClassId", new { classId, parentId = importedObjectsNodeFolderNodeId, personId }, transaction: scope.Transaction);
    }

    public void AddMissingPropsToAmTemplateAndUnify(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 #kinds (id)
select id from dbo.ftCommaListToTableIds(@kindCodes)

--insert new to template
insert into ClassKind (KindId, ClassId)
select
	k.intKindId,
	@classId classId
from #kinds ids
	join tblKind k on k.intKindCode = ids.id
	left join ClassKind ck on ck.ClassId = @classId and k.intKindId = ck.KindId
where ck.KindId is null
", new { classId, kindCodes }, transaction: scope.Transaction);
    }

    public void CreateConnectorScannerIfNotExists()
    {
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute($@"
            IF NOT EXISTS (
                SELECT 1 FROM Scanner WHERE id = {(int)ScannerId.Splashtop}
            )
            BEGIN
                INSERT INTO Scanner (id, Name)
                VALUES ({(int)ScannerId.Splashtop}, N'Splashtop');
            END
            ");
    }

    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.Splashtop}
            and ck.KindId is null");
    }

    public void RecalculateScannerPropertyLockoutTable(SplashtopDevice device, int classId)
    {
        if (classId == 0)
            return;

        if (ScannerPropMapCache.Contains(classId))
            return;

        KindCode lockingKindCode = KindCode.SplashtopDeviceId;
        string lockedKindCodes = string.Join(",", SplashtopDevice.GetPropertyMappingForType(device.DeviceType).Select(kv =&gt; kv.Key).Where(k =&gt; k != lockingKindCode).Select(k =&gt; ((int)k).ToString()));
        RecalculateScannerPropertyLockoutByClass(classId, (int)lockingKindCode, lockedKindCodes);

        ScannerPropMapCache.Add(classId);
    }

        public 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, notInTemplate bit default 0)
            insert #locked (id)
            select id from dbo.ftCommaListToTableIds(@lockedKindCodes)

            merge into ScannerPropertyLockout tar using (
                select
                    {(int)ScannerId.Splashtop},
                    @classId,
                    locking.intKindId,
                    locked.intKindId
                from tblKind locked
                    join #locked l on l.id=locked.intKindCode
                    join tblKind locking on locking.intKindCode=@lockingKindCode
            ) src (ScannerId,ClassId,LockingKindId,LockedKindId) on 
                tar.ScannerId=src.ScannerId 
                and tar.ClassId=src.ClassId 
                and tar.LockingKindId=src.LockingKindId
                and tar.LockedKindId=src.LockedKindId
            when not matched by target then insert (ScannerId,ClassId,LockingKindId,LockedKindId) values (ScannerId,ClassId,LockingKindId,LockedKindId)
            when not matched by source and tar.ClassId=@classId and tar.ScannerId={(int)ScannerId.Splashtop} then delete;", new { classId, lockingKindCode, lockedKindCodes }, transaction: scope.Transaction);
    }

    public void InsertIntoObjectLog(tblLog log)
    {
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute(@"
            insert into tblLog (lintNodeId, liLogPersonId, dteLog, txtLog)
            values (@lintNodeId, @liLogPersonId, @dteLog, @txtLog)   
        ", 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 class SplashtopClientService
{
    private string Token { get; set; }
    private static readonly HttpClient client = new HttpClient();

    private Task&lt;HttpRequestMessage&gt; PrepareRequest(HttpMethod method, string url, object content = null)
    {
        var completeUrl = $"{Misc.SplashtopUrl}/{url}";
        var httpRequest = new HttpRequestMessage(method, completeUrl);
        httpRequest.Headers.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DbProperty.SplashtopToken);

        if(content != null){
            DefaultContractResolver contractResolver = new DefaultContractResolver
            {
                NamingStrategy = new CamelCaseNamingStrategy()
            };
            var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = contractResolver, };

            var strContent =  JsonConvert.SerializeObject(content, settings);

            httpRequest.Content = new StringContent(strContent, Encoding.UTF8, "application/json");
        }
        return Task.FromResult(httpRequest);
    }

    private Task&lt;string&gt; GetToken()
    {
        return Task.FromResult(DbProperty.SplashtopToken);
    }

    public async Task&lt;int[]&gt; GetDeviceIds(int teamId)
    {
        var request = await PrepareRequest(HttpMethod.Get, $"teams/{teamId}/computers", null);
        var response = await client.SendAsync(request);

        string responseContent = await response.Content.ReadAsStringAsync();
        var responseObject = JsonConvert.DeserializeObject&lt;JObject&gt;(responseContent);

        return responseObject["data"].Select(item =&gt; (int)item["id"]).ToArray() ?? Array.Empty&lt;int&gt;();
    }

    public async Task&lt;IEnumerable&lt;SplashtopDevice&gt;&gt; GetNewOrUpdatedDevices(CancellationToken cancellationToken)
    {
        int teamId = await Misc.GetTeamId();
        var ids = await GetDeviceIds(teamId);
        var requestBody = new Dictionary&lt;string, object&gt;
        {
            { "ids", ids },
            { "fields", new[] { "hardware" } }
        };

        var request = await PrepareRequest(HttpMethod.Post, $"teams/{teamId}/inventory", requestBody);

        var response = await client.SendAsync(request);
        var responseContent = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            var responseObject = JsonConvert.DeserializeObject&lt;SplashtopResponse&gt;(responseContent);
            if (responseObject.Error != null)
            {
                Logger.Log.Error($"Splashtop error [{responseObject.Error.Code}]: {responseObject.Error.Message} - {responseObject.Error.Data}");
                throw new Exception($"Splashtop error [{responseObject.Error.Code}]: {responseObject.Error.Message}.");
            }
            var devices = responseObject.Data;
            if (devices == null)
                return new List&lt;SplashtopDevice&gt;();
            return devices.Select(d =&gt; d.ToSplashtopDevice()).ToList();
        }
        else
        {
            throw new Exception($"Unsuccessful response from {PeriodicAction.ConnectorName}: {response.StatusCode} {responseContent}");
        }
    }
}

// MODELS
public record SplashtopResponse(int Result, IEnumerable&lt;SplashtopApiDevice&gt; Data, SplashtopError Error);
public record Hardware(string manufacturer, Memory memory, Motherboard motherboard, List&lt;Drive&gt; drive);
public record Memory(int size);
public record Motherboard(string bios_serial_number, string serial_number);
public record Drive(List&lt;Partition&gt; partition);
public record Partition(int capacity);

public class SplashtopError
{
    public int Code { get; set; }
    public string Message { get; set; }
    public object Data { get; set; }
}

public class SplashtopApiDevice
{
    public int id { get; set; }
    public string server_name { get; set; }
    public Hardware hardware { get; set; }

    public SplashtopDevice ToSplashtopDevice()
    {
        return new SplashtopDevice()
        {
            SplashtopDeviceId = id.ToString(),
            Hostname = server_name,
            SerialNo = hardware.motherboard.serial_number,
            BIOS_SN = hardware.motherboard.bios_serial_number,
            Manufacturer = hardware.manufacturer,
            RAMCapacityInGB = hardware.memory.size / 1024,
            TotalStorageSpaceInGB = hardware.drive.SelectMany(d =&gt; d.partition).Sum(p =&gt; p.capacity) / 1024,
            DeviceType = SplashtopDevice.Type.Computer
        };
    }

    public DateTime DateTimeFromEpoch(long ticks){
        return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromSeconds(ticks);
    }
}

public class SplashtopDevice
{
    public enum Type
    {
        Computer
    }
    public string SplashtopDeviceId { get; set; }
    
    public string Hostname { get; set; }
    public string SerialNo { get; set; }
    public string BIOS_SN { get; set; }
    public string Manufacturer { get; set; }
    public int RAMCapacityInGB { get; set; }
    public int TotalStorageSpaceInGB { get; set; }
    public Type DeviceType { get; set; }
    public int? NodeId { get; set; }

    public static Dictionary&lt;tblClass.ClassCode, Type&gt; ClassMap = new()
    {
        { tblClass.ClassCode.Computer, Type.Computer }
    };

    public static int? GetFirstClassIdForDeviceType(Type type)
    {
        foreach(var classId in ClassMap.Keys)
        {
            if (ClassMap[classId] == type)
                return (int)classId;
        }
        return null;
    }

    public Dictionary&lt;KindCode, string&gt; GetPropertyMapping()
    {
        return GetPropertyMappingForType(DeviceType);
    }

    public static Dictionary&lt;KindCode, string&gt; GetPropertyMappingForType(Type type)
    {
        Dictionary&lt;KindCode, string&gt; mapping = new()
        {
            {KindCode.SplashtopDeviceId, nameof(SplashtopDeviceId)},
            {KindCode.HostName, nameof(Hostname)},
            {KindCode.SerialNo, nameof(SerialNo)},
            {KindCode.BIOS_SN, nameof(BIOS_SN)},
            {KindCode.Manufacturer, nameof(Manufacturer)},
            {KindCode.RAMCapacityInGB, nameof(RAMCapacityInGB)},
            {KindCode.TotalStorageSpaceInGB, nameof(TotalStorageSpaceInGB)}
        };

        return mapping;
    }

    public override string ToString()
    {
        return $"SplashtopDevice [\n" +
       $"  SplashtopDeviceId={SplashtopDeviceId},\n" +
       $"  Hostname={Hostname},\n" +
       $"  SerialNo={SerialNo},\n" +
       $"  BIOS_SN={BIOS_SN},\n" +
       $"  Manufacturer={Manufacturer},\n" +
       $"  RAMCapacityInGB={RAMCapacityInGB},\n" +
       $"  TotalStorageSpaceInGB={TotalStorageSpaceInGB},\n" +
       $"  DeviceType={DeviceType},\n" +
       $"]";
    }
}

internal static class ObjectWbemProcess
{
    internal class WbemProp
    {
        public string Class { get; set; }
        public string Name { get; set; }

        public WbemProp(string txtclass, string txtname)
        {
            Class = txtclass;
            Name = txtname;
        }
    }
    private static Dictionary&lt;int, WbemProp&gt; KindToWbemMap = new()
        {
            {87, new("BIOS", "SerialNumber") }
        };
    internal static WbemProp GetWbemEquivalentNameAndClass(int kindCode)
    {
        WbemProp wbemProp = null;
        KindToWbemMap.TryGetValue(kindCode, out wbemProp);
        return wbemProp;
    }
}

/*Groups incoming devices based on Alvao object they matched to. Devices that do not exist in Alvao yet are matched to object ID 0.
     * Collisions are detected when two or more incoming devices match to the same Alvao object (ID &gt; 0) or when two or more incoming devices do not match to any Alvao object (ID = 0) but match each other based on BIOS SN or Hostname.
     */
public class ImportCollisionChecker
{
    private Dictionary&lt;int, CollisionInfo&gt; MatchMap { get; set; } = new Dictionary&lt;int, CollisionInfo&gt;();

    public void AddImportedInfo(int nodeId, SplashtopDevice SplashtopDevice, bool? errorSetBeforeImport = null)
    {
        var isFirstOccurence = !MatchMap.ContainsKey(nodeId);
        if (isFirstOccurence)
        {
            MatchMap.Add(nodeId, new CollisionInfo());
        }
        var collisionInfo = MatchMap[nodeId];
        if (errorSetBeforeImport is not null &amp;&amp; isFirstOccurence) collisionInfo.PresentBeforeImport = errorSetBeforeImport.Value;
        collisionInfo.CollidingObjects.Add(SplashtopDevice);
    }

    public bool ShouldLogCollision(int nodeId)
    {
        return nodeId &gt; 0 &amp;&amp; HasCollision(nodeId) &amp;&amp; !HadCollisionBeforeImportStarted(nodeId);
    }

    public bool HasCollision(int nodeId)
    {
        MatchMap.TryGetValue(nodeId, out var collisionInfo);
        if (collisionInfo is null)
        {
            return false;
        }
        return collisionInfo.CollidingObjects.Count &gt;= 2;
    }

    public bool HadCollisionBeforeImportStarted(int nodeId)
    {
        MatchMap.TryGetValue(nodeId, out var collisionInfo);
        return collisionInfo?.PresentBeforeImport ?? false;
    }

    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()
    {
        var collisionsOnExistingObjects = MatchMap.Where(it =&gt; it.Key &gt; 0 &amp;&amp; it.Value.CollidingObjects.Count &gt;= 2).ToDictionary(); // two incoming devices matched on one existing AM object (does not matter by which parameter)

        if (!MatchMap.ContainsKey(0))
        {
            return collisionsOnExistingObjects;
        }

        var collisionsOnObjectsToCreateByBiosSn = MatchMap[0].CollidingObjects.GroupBy(o =&gt; o.SerialNo).Where(gr =&gt; gr.Count() &gt;= 2).SelectMany(gr =&gt; gr); // two incoming devices do not have matching AM object yet, but they match each other based on BIOS SN or Hostname
        var collisionsOnObjectsToCreateByHostname = MatchMap[0].CollidingObjects.GroupBy(o =&gt; o.Hostname).Where(gr =&gt; gr.Count() &gt;= 2).SelectMany(gr =&gt; gr);

        var collisionsOnObjectsToCreate = collisionsOnObjectsToCreateByBiosSn.Union(collisionsOnObjectsToCreateByHostname).DistinctBy(d =&gt; d.SplashtopDeviceId).ToList();
        return collisionsOnExistingObjects.Concat(new Dictionary&lt;int, CollisionInfo&gt;()
            {
                { 0, new CollisionInfo() { PresentBeforeImport = false, CollidingObjects = collisionsOnObjectsToCreate } }
            }).ToDictionary();
    }

    public IEnumerable&lt;int&gt; GetNodesToUnsetCollisionFlag()
    {
        return MatchMap.Where(it =&gt; it.Key &gt; 0 &amp;&amp; it.Value.CollidingObjects.Count == 1 &amp;&amp; it.Value.PresentBeforeImport).Select(it =&gt; it.Key);
    }

    private string GetCollisionLogMessage(int nodeId)
    {
        var sb = new StringBuilder();
        sb.Append("Duplicate devices were found during import. Splashtop device IDs: ");

        var ids = MatchMap[nodeId].CollidingObjects.Select(o =&gt; o.SplashtopDeviceId).ToList();
        var threeDots = "";
        if (ids.Count &gt; 3) threeDots = "...";

        var idsPart = string.Join(", ", ids.Take(3));
        sb.Append($"{idsPart}{threeDots}");
        return sb.ToString();
    }

    public class CollisionInfo
    {
        public bool PresentBeforeImport { get; set; } = false;
        public List&lt;SplashtopDevice&gt; CollidingObjects { get; set; } = [];
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>n866UoHUXtfSXSx7FWV/OdQyNgGrGpPxPzDvgMk+tB+HsUaAquiqyyuPJ6KlY2o4ZYjGlqHsS2vRgVE/8jxSVlvvgKJLLigmAcpxEXcEZ4QktQ+tK7nRyi5tSKsWsMIj4etwLqMjEeQWuz+QuVfN0EuJyzbQ7PQjKr2/7T8bkGlsb5CYw8rzew2TFwU+cH7dAaIldEYS1yzbL8WCcFM/+JSEn1CBU78nKG1KSSzHIz1GcoXEfltCUUScAfpI/XAksAKVh4RXCXPCkLEeDyRNsxKMBVF4OVHQOZSwKajjpH5P2EXez9j8gxxXdb9IGVKLNZM9gF/+0CeM88k8PjgA8A==</Codesign>
        </Script>
        <Script id="145">
          <Name>OpenObjectInSplashtopEntityCommand</Name>
          <Code>using System;
using Alvao.API.Common.Model.CustomApps;
using Alvao.API.Common;
using Alvao.Apps.API;
using Alvao.Context;
using Dapper;
using Alvao.API.Common.Model.Database;

namespace Alvao.CustomApps.SplashtopConnector;

public class OpenInSplashtopObjectCommand : IEntityCommand 
{
    public string Id {get; set;}
    public Entity Entity {get; set;}

    public OpenInSplashtopObjectCommand()
    {
        Id = "OpenInSplashtopObjectCommand";
        Entity = Entity.Object;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {   
        int position = 2; 
        string icon = "open_20_regular";
        string name = "Open in Splashtop"; 
        bool show = CanShow(entityId, personId); 
        return new EntityCommandShowResult (show, name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        MessageType messageType = MessageType.None;
        string messageText = null; 
        string navigateToUrl = $"https://my.splashtop.eu/property/general/{GetSplashtopId(entityId)}";
        return new CommandResult(messageType, messageText, navigateToUrl);
    }
    
    private int? GetSplashtopId(int objectId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        
        return scope.Connection.ExecuteScalar&lt;int?&gt;($@"
                select SplashtopDeviceId from NodeCust where NodeId=@objectId", new { objectId }, scope.Transaction);
    }

    private bool CanShow(int objectId, int personId)
    {
        if (GetSplashtopId(objectId) == null)
            return false;
        return (PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetManagers, false) ||
                PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetAdministrators, false));
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>sZX5KyXk7n/lzTkOIYMdoNysYokhBOLK04rN9RTQODdvga6HZQ8wYH9N7Zwu9YI5gUGf/bmCHTIrGmM4yGgEKkccLahiDy8oIeG9e0PSIqwA0qNCZXutN278vkHQCv3VlidLrp7Xp2SBtuDdAjr3iD8FFCuWlRDKLRwNl3F5VApGqP4AoZiwrqnBdfBX6BY+lm9jquWbhUSOv95d3kBD/NaELL2bO5ZCy1u5MlXhO/4ToxRVzjihNkbF8mM7aRqGHgQ+kqJKrBloNyPsauMF1IfP4o7OGYXp28T6Bmtl1a80+rK8PqNUtwMIvIhgBXrX6a4PjWW1NMw1rwwpoPEUVg==</Codesign>
        </Script>
        <Script id="146">
          <Name>StartRemoteSplashtopSessionEntityCommand</Name>
          <Code>
            using System;
            using System.Linq;
            using Alvao.API.Common;
            using Alvao.API.Common.Model.CustomApps;
            using Alvao.Apps.API;
            using Alvao.Context;
            using Alvao.API.Common.Model.Database;
            using Dapper;
            using System.Threading.Tasks;
            using System.Net.Http;
            using Newtonsoft.Json;
            using Newtonsoft.Json.Linq;

            namespace Alvao.CustomApps.SplashtopConnector;

            public class StartRemoteSplashtopSessionObjectCommand : IEntityCommand
            {
            public string Id {get; set;}
            public Entity Entity {get; set;}

            public StartRemoteSplashtopSessionObjectCommand()
            {
            Id = "StartRemoteSplashtopSessionObjectCommand";
            Entity = Entity.Object;
            }


            public EntityCommandShowResult Show(int entityId, int personId)
            {
            int position = 2;
            string icon = "wifi_1_20_regular";
            string name = "Splashtop session";
            bool show = CanShow(entityId, personId);
            return new EntityCommandShowResult (show, name, icon, position);
            }

            public CommandResult Run(int entityId, int personId)
            {
            MessageType messageType = MessageType.None;
            string messageText = null;
            string uuid = GetSplashtopUuid(entityId).Result;
            if (string.IsNullOrEmpty(uuid))
            {
              messageType = MessageType.Error;
              messageText = "Could not retrieve device information from Splashtop. Please contact your administrator.";
              return new CommandResult(messageType, messageText, null);
            }
            string navigateToUrl = $"st-business://com.splashtop.business/?uuid={uuid}";
            LogConnection(entityId, personId);
            return new CommandResult(messageType, messageText, navigateToUrl);
            }

            private void LogConnection(int nodeId, int personId)
            {
            string txtLog = "Splashtop: Unattended session initiated";
            var date = DateTime.UtcNow;
            using var scope = AlvaoContext.GetConnectionScope();
            scope.Connection.Execute(@"
            insert into tblLog (lintNodeId, liLogPersonId, dteLog, txtLog, lintLogKindId)
            values (@nodeId, @personId, @date, @txtLog, 1)
            ", new {nodeId, personId, date, txtLog}, transaction: scope.Transaction);
            }

            private int? GetSplashtopId(int objectId)
            {
            using(var scope = AlvaoContext.GetConnectionScope())

            return scope.Connection.ExecuteScalar&lt;int?&gt;($@"
                select SplashtopDeviceId from NodeCust where NodeId=@objectId", new { objectId }, scope.Transaction);
    }

    private async Task&lt;string&gt; GetSplashtopUuid(int objectId)
    {
        HttpClient client = new HttpClient();
        int teamId = await Misc.GetTeamId();
        int splashtopId = GetSplashtopId(objectId).Value;
        string url = Misc.SplashtopUrl + $"teams/{teamId}/computers?ids={splashtopId}";
        var uuidRequest = new HttpRequestMessage(HttpMethod.Get, url);
        uuidRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DbProperty.SplashtopToken);

        var uuidResponse = await client.SendAsync(uuidRequest);
        string uuidResponseContent = await uuidResponse.Content.ReadAsStringAsync();
        var uuidResponseObject = JsonConvert.DeserializeObject&lt;JObject&gt;(uuidResponseContent);
        
        string uuid = "";
        if (uuidResponseObject["data"] is JArray arr &amp;&amp; arr.Count &gt; 0)
        {
            uuid = (string)arr[0]?["uuid"] ?? "";
        }
        return uuid;
    }

    private bool CanShow(int objectId, int personId)
    {
        if (GetSplashtopId(objectId) == null || string.IsNullOrEmpty(DbProperty.SplashtopToken))
            return false;
        return (PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetManagers, false) ||
                PersonRights.HaveGlobalRole(personId, RoleBehavior.BehaviorId.AssetAdministrators, false));
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>B26E+bk5EpSzT808mWdcbEwlX1pQrmw2uZZ0YTcvXdJeXlvgi6nQAinsj7kOwvuHzkFzVu9LJUB1pwTCeFrH4b2Oo0RWIxOA+Hp40hAgXCCtjXwKoWL1xrQCtmcvsFRYo1VqRHSo3yGlLY3NEJLXf8yyOFoOvXu+bhNyCJrfkMAsFoXPwPAdSu3pJJbopp4/Clao4KRj7ip2BB4i9LWHDA3XNKOHHx5QyLJBnAlHN/kj/4csxo7azSyrVNjl6YQltzSZTPoE6RLiNoHDAP2Mxh3ABCNHvgMYDo5ZqhpyqrwZ2V1OystJxyM87vrq5vXGa55xhcfhbp+W66sUfZjnRQ==</Codesign>
        </Script>
        <Script id="147">
          <Name>RequestSplashtopSessionViaTeamsEntityCommand</Name>
          <Code>using System;
using System.Linq;
using Alvao.API.Common.Model.CustomApps;
using Alvao.Apps.API;
using Alvao.Context;
using Dapper;

namespace Alvao.CustomApps.SplashtopConnector;

public class RequestSplashtopSessionViaTeamsEntityCommand : IEntityCommand 
{
    public string Id {get; set;}
    public Entity Entity {get; set;}

    public RequestSplashtopSessionViaTeamsEntityCommand()
    {
        Id = "RequestSplashtopSessionViaTeamsEntityCommand";
        Entity = Entity.Request;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {   
            int position = 4;
            string icon = "mail_20_regular";
            string name = "Request Splashtop session via Teams";
            bool show = CanShow(entityId, personId);
            return new EntityCommandShowResult (show, name, icon, position);
        throw new NotImplementedException();
    }

    public CommandResult Run(int entityId, int personId)
    {
            MessageType messageType = MessageType.None;
            string messageText = GetMessage(entityId);
            string requesterEmail = GetRequesterEmail(entityId);
            string navigateToUrl = $"https://teams.microsoft.com/l/chat/0/0?users={requesterEmail}&amp;message={messageText}";
            LogRequest(Alvao.API.SD.Relation.GetLinkedNodes(entityId).First(), personId);
            return new CommandResult(messageType, messageText, navigateToUrl);
        
    }

    private void LogRequest(int nodeId, int personId)
    {
        string txtLog = "Splashtop: Attended session requested";
        var date = DateTime.UtcNow;
        using var scope = AlvaoContext.GetConnectionScope();
        scope.Connection.Execute(@"
            insert into tblLog (lintNodeId, liLogPersonId, dteLog, txtLog, lintLogKindId)
            values (@nodeId, @personId, @date, @txtLog, 1)   
        ", new {nodeId, personId, date, txtLog}, transaction: scope.Transaction);  
    }

    private bool CanShow(int ticketId, int personId)
    {
        var ticket = Alvao.API.SD.Ticket.GetById(ticketId);

        int computerCount = 0;
        var objectIds = Alvao.API.SD.Relation.GetLinkedNodes(ticketId);
        foreach (int objectId in objectIds) {
            if (Alvao.API.AM.Object.IsComputer(objectId) &amp;&amp; ++computerCount &gt; 1)
                return false;
        }

        if (computerCount != 1)
            return false;

        if (personId != ticket.liHdTicketSolverPersonId)
            return false;

        if (Alvao.API.SD.TicketParticipant.Get(ticketId, true).Any())
            return false;

        return true;
    }

    private string GetMessage(int ticketId)
    {
        int preferredLanguage = getPreferredLanguage(ticketId);
        string message = "";

        if (preferredLanguage == 1029) {
            message = @"Ahoj,
prosím otevři odkaz https://sos.splashtop.eu
Stáhni a spusť program z tohoto odkazu. Zobrazí se ti 9místný kód schůzky.
Nech program spuštěný a pošli mi tento kód, abych se mohl připojit k tvému zařízení.
Děkuji,
Administrátor";
        } else {
            message = @"Hi,
Please open the link https://sos.splashtop.eu
Download and run the program from the link. It will show you a 9-digit session code.
Keep the program running and send me the code so I can connect to your device.
Thank you,
Administrator";
        }
        return Uri.EscapeDataString(message);
    }

    private string GetRequesterEmail(int ticketId)
    {
        int requesterId = Alvao.API.SD.Ticket.GetById(ticketId).liHdTicketUserPersonId;
        string mail = Alvao.API.Common.Person.GetById(requesterId).sPersonEmail;
        return Uri.EscapeDataString(mail);
    }

    private int getPreferredLanguage(int ticketId)
    {
        int requesterId = Alvao.API.SD.Ticket.GetById(ticketId).liHdTicketUserPersonId;
        return Alvao.API.Common.Person.GetCultureInfoOrDefaultFromId(requesterId).LCID;
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>OkCL8krO8sgkGtJui9CDC2Xx+JksHoz4n1XKlC2Zu/jCYGRk+7AnmOTVPsd+bdnAsBa3WQdAt/Fwc69BB+D3viV9RY43zBrCjAMWWXjHP817I49i5E6RhCeLtVim6+xxzGAPUYvp28ozcnNaRCEhZvqzZ3Hl1rgPL4envqifPcNYDKVJDkn7qvoXzbyIaYJKM/j/5Wd8MxAbNfcYfHmmgTibzduvJ0G1HkKSQJqII39jMaMKUO4awQ1i3DtnkMEed/rDsbt8nnKOxbOIajPLPXPj9Sdl5a1zYxWYyJ+3QryV9zDbsPIDoLbIOdgDuv46mT5GgvYcWmnFLPHb5dQoww==</Codesign>
        </Script>
        <Script id="148">
          <Name>Misc</Name>
          <Code>using NLog;
using Alvao.API.Internal;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Alvao.API.Common;
namespace Alvao.CustomApps.SplashtopConnector;

public static class Logger
{
    public static ILogger Log = TenantDiagnosticsLog.Get();
}

public static class Misc
{
    public static string SplashtopUrl = "https://webapi.splashtop.eu/api/open/v1/";

    public async static Task&lt;int&gt; GetTeamId()
    {
        HttpClient client = new HttpClient();

        var url = SplashtopUrl + "users/basic_info";
        var teamIdRequest = new HttpRequestMessage(HttpMethod.Get, url);
        teamIdRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DbProperty.SplashtopToken);

        var teamIdResponse = await client.SendAsync(teamIdRequest);

        string teamIdResponseContent = await teamIdResponse.Content.ReadAsStringAsync();
        var teamIdResponseObject = JsonConvert.DeserializeObject&lt;JObject&gt;(teamIdResponseContent);
        int teamId = (int)teamIdResponseObject["data"]["scopes"]["psa"]["team_id"];
        return teamId;
    }
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>V+wQgHf96b2oCgzpUpW5qRuCWG5T5tvVkWxBIi960+thX0Wk4zvWcgmFFGlRdzr92agBkjy4uZ0/2kCjsogqhOesb+2E0Q0nCIluBvs2urxeEE+SSfqm+jVfShtqP2VsQnJqc3osm7KDSLJu7XNGtl6PLOQ54lxruTodxlHME7HhWvfpQ4rDU7VwhgeUSwrQeZVHAnlaNScKW+gwHE0vZ7mB4zJiwnpI0iotKze76TRUHx1is1n3hE8VohBvX7qCIwwUYN4YlNPQ/VAqMeVmG2Z0XaSS7zZ5LokKrDaVZkyvk05lQ044s0iN9f3IO8zFOcBLFB3FFJU9t2mZcn0HxQ==</Codesign>
        </Script>
      </Scripts>
    </Application>
  </Applications>
</AlvaoApplication>