using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace LibreMetaverse.RLV { internal sealed class RlvGetRequestHandler { private readonly ImmutableDictionary _rlvDataRequestToNameMap; private readonly IRestrictionProvider _restrictions; private readonly IBlacklistProvider _blacklist; private readonly IRlvQueryCallbacks _queryCallbacks; private readonly IRlvActionCallbacks _actionCallbacks; internal RlvGetRequestHandler(IBlacklistProvider blacklist, IRestrictionProvider restrictions, IRlvQueryCallbacks queryCallbacks, IRlvActionCallbacks actionCallbacks) { _restrictions = restrictions; _blacklist = blacklist; _queryCallbacks = queryCallbacks; _actionCallbacks = actionCallbacks; _rlvDataRequestToNameMap = new Dictionary() { { "getcam_avdistmin", RlvDataRequestType.GetCamAvDistMin }, { "getcam_avdistmax", RlvDataRequestType.GetCamAvDistMax }, { "getcam_fovmin", RlvDataRequestType.GetCamFovMin }, { "getcam_fovmax", RlvDataRequestType.GetCamFovMax }, { "getcam_zoommin", RlvDataRequestType.GetCamZoomMin }, { "getcam_fov", RlvDataRequestType.GetCamFov }, { "getsitid", RlvDataRequestType.GetSitId }, { "getoutfit", RlvDataRequestType.GetOutfit }, { "getattach", RlvDataRequestType.GetAttach }, { "getinv", RlvDataRequestType.GetInv }, { "getinvworn", RlvDataRequestType.GetInvWorn }, { "findfolder", RlvDataRequestType.FindFolder }, { "findfolders", RlvDataRequestType.FindFolders }, { "getpath", RlvDataRequestType.GetPath }, { "getpathnew", RlvDataRequestType.GetPathNew }, { "getgroup", RlvDataRequestType.GetGroup } }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase); } private string HandleGetStatus(string option, Guid? sender) { var parts = option.Split(';'); var filter = string.Empty; var separator = "/"; if (parts.Length > 0) { filter = parts[0].ToLowerInvariant(); } if (parts.Length > 1) { separator = parts[1]; } var restrictions = _restrictions.FindRestrictions(filter, sender); StringBuilder sb = new StringBuilder(); foreach (var restriction in restrictions) { if (!RlvRestrictionManager.TryGetRestrictionNameFromType(restriction.OriginalBehavior, out var behaviorName)) { continue; } sb.Append($"{separator}{behaviorName}"); if (restriction.Args.Count > 0) { sb.Append($":{string.Join(";", restriction.Args)}"); } } return sb.ToString(); } private async Task ProcessGetOutfit(RlvWearableType? specificType, CancellationToken cancellationToken) { var (hasCurrentOutfit, currentOutfit) = await _queryCallbacks.TryGetCurrentOutfitAsync(cancellationToken).ConfigureAwait(false); if (!hasCurrentOutfit || currentOutfit == null) { return string.Empty; } if (specificType != null) { if (currentOutfit.Where(n => n.WornOn == specificType).Any()) { return "1"; } else { return "0"; } } var wornTypes = currentOutfit .Where(n => n.WornOn.HasValue) .Select(n => n.WornOn!.Value) .Distinct() .ToImmutableHashSet(); var sb = new StringBuilder(16); // gloves,jacket,pants,shirt,shoes,skirt,socks,underpants,undershirt,skin,eyes,hair,shape,alpha,tattoo,physics sb.Append(wornTypes.Contains(RlvWearableType.Gloves) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Jacket) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Pants) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Shirt) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Shoes) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Skirt) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Socks) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Underpants) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Undershirt) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Skin) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Eyes) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Hair) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Shape) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Alpha) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Tattoo) ? "1" : "0"); sb.Append(wornTypes.Contains(RlvWearableType.Physics) ? "1" : "0"); return sb.ToString(); } private async Task ProcessGetAttach(RlvAttachmentPoint? specificType, CancellationToken cancellationToken) { var (hasCurrentOutfit, currentOutfit) = await _queryCallbacks.TryGetCurrentOutfitAsync(cancellationToken).ConfigureAwait(false); if (!hasCurrentOutfit || currentOutfit == null) { return string.Empty; } if (specificType != null) { if (currentOutfit.Where(n => n.AttachedTo == specificType).Any()) { return "1"; } else { return "0"; } } var wornTypes = currentOutfit .Where(n => n.AttachedTo.HasValue) .Select(n => n.AttachedTo!.Value) .Distinct() .ToImmutableHashSet(); var attachmentPointTypes = Enum.GetValues(typeof(RlvAttachmentPoint)); var sb = new StringBuilder(attachmentPointTypes.Length); // digit corresponds directly to the value of enum, unlike ProcessGetOutfit foreach (RlvAttachmentPoint attachmentPoint in attachmentPointTypes) { sb.Append(wornTypes.Contains(attachmentPoint) ? '1' : '0'); } return sb.ToString(); } internal async Task ProcessGetCommand(RlvMessage rlvMessage, int channel, CancellationToken cancellationToken) { var blacklist = _blacklist.GetBlacklist(); string? response = null; switch (rlvMessage.Behavior) { case "version": case "versionnew": response = RlvService.RLVVersion; break; case "versionnum": response = RlvService.RLVVersionNum; break; case "versionnumbl": if (blacklist.Count > 0) { response = $"{RlvService.RLVVersionNum},{string.Join(",", blacklist)}"; } else { response = RlvService.RLVVersionNum; } break; case "getblacklist": var filteredBlacklist = blacklist .Where(n => n.Contains(rlvMessage.Option)); response = string.Join(",", filteredBlacklist); break; case "getstatus": response = HandleGetStatus(rlvMessage.Option, rlvMessage.Sender); break; case "getstatusall": response = HandleGetStatus(rlvMessage.Option, null); break; } if (_rlvDataRequestToNameMap.TryGetValue(rlvMessage.Behavior, out var name)) { switch (name) { case RlvDataRequestType.GetSitId: { var (hasSitId, sitId) = await _queryCallbacks.TryGetSitIdAsync(cancellationToken).ConfigureAwait(false); if (!hasSitId || sitId == Guid.Empty) { response = "NULL_KEY"; } else { response = sitId.ToString(); } break; } case RlvDataRequestType.GetCamAvDistMin: case RlvDataRequestType.GetCamAvDistMax: case RlvDataRequestType.GetCamFovMin: case RlvDataRequestType.GetCamFovMax: case RlvDataRequestType.GetCamZoomMin: case RlvDataRequestType.GetCamFov: { var (hasCameraSettings, cameraSettings) = await _queryCallbacks.TryGetCameraSettingsAsync(cancellationToken).ConfigureAwait(false); if (!hasCameraSettings || cameraSettings == null) { return false; } switch (name) { case RlvDataRequestType.GetCamAvDistMin: { response = cameraSettings.AvDistMin.ToString(CultureInfo.InvariantCulture); break; } case RlvDataRequestType.GetCamAvDistMax: { response = cameraSettings.AvDistMax.ToString(CultureInfo.InvariantCulture); break; } case RlvDataRequestType.GetCamFovMin: { response = cameraSettings.FovMin.ToString(CultureInfo.InvariantCulture); break; } case RlvDataRequestType.GetCamFovMax: { response = cameraSettings.FovMax.ToString(CultureInfo.InvariantCulture); break; } case RlvDataRequestType.GetCamZoomMin: { response = cameraSettings.ZoomMin.ToString(CultureInfo.InvariantCulture); break; } case RlvDataRequestType.GetCamFov: { response = cameraSettings.CurrentFov.ToString(CultureInfo.InvariantCulture); break; } } break; } case RlvDataRequestType.GetGroup: { var (hasGroup, group) = await _queryCallbacks.TryGetActiveGroupNameAsync(cancellationToken).ConfigureAwait(false); if (!hasGroup) { response = "none"; } else { response = group; } break; } case RlvDataRequestType.GetOutfit: { RlvWearableType? wearableType = null; if (RlvCommon.RlvWearableTypeMap.TryGetValue(rlvMessage.Option, out var wearableTypeTemp)) { wearableType = wearableTypeTemp; } response = await ProcessGetOutfit(wearableType, cancellationToken).ConfigureAwait(false); break; } case RlvDataRequestType.GetAttach: { RlvAttachmentPoint? attachmentPointType = null; if (RlvCommon.RlvAttachmentPointMap.TryGetValue(rlvMessage.Option, out var attachmentPointTemp)) { attachmentPointType = attachmentPointTemp; } response = await ProcessGetAttach(attachmentPointType, cancellationToken).ConfigureAwait(false); break; } case RlvDataRequestType.GetInv: response = await HandleGetInv(rlvMessage.Option, cancellationToken).ConfigureAwait(false); break; case RlvDataRequestType.GetInvWorn: response = await HandleGetInvWorn(rlvMessage.Option, cancellationToken).ConfigureAwait(false); break; case RlvDataRequestType.FindFolder: case RlvDataRequestType.FindFolders: { var findFolderParts = rlvMessage.Option.Split(';'); var separator = ","; var searchTerms = findFolderParts[0] .Split(["&&"], StringSplitOptions.RemoveEmptyEntries); if (findFolderParts.Length > 1) { separator = findFolderParts[1]; } response = await HandleFindFolders(name == RlvDataRequestType.FindFolder, searchTerms, separator, cancellationToken).ConfigureAwait(false); break; } case RlvDataRequestType.GetPath: case RlvDataRequestType.GetPathNew: { // [] | [attachedPrimId | layer | attachpt ] var parsedOptions = rlvMessage.Option.Split([';'], StringSplitOptions.RemoveEmptyEntries).ToList(); if (parsedOptions.Count > 1) { return false; } if (parsedOptions.Count == 0) { var (hasSharedFolder, sharedFolder) = await _queryCallbacks.TryGetSharedFolderAsync(cancellationToken).ConfigureAwait(false); if (!hasSharedFolder || sharedFolder == null) { return false; } var inventoryMap = new InventoryMap(sharedFolder); var folderPaths = new List(); response = await HandleGetPath(name == RlvDataRequestType.GetPath, rlvMessage.Sender, null, null, cancellationToken).ConfigureAwait(false); } else if (Guid.TryParse(parsedOptions[0], out var attachedPrimId)) { response = await HandleGetPath(name == RlvDataRequestType.GetPath, attachedPrimId, null, null, cancellationToken).ConfigureAwait(false); } else if (RlvCommon.RlvWearableTypeMap.TryGetValue(parsedOptions[0], out var wearableType)) { response = await HandleGetPath(name == RlvDataRequestType.GetPath, null, null, wearableType, cancellationToken).ConfigureAwait(false); } else if (RlvCommon.RlvAttachmentPointMap.TryGetValue(parsedOptions[0], out var attachmentPoint)) { response = await HandleGetPath(name == RlvDataRequestType.GetPath, null, attachmentPoint, null, cancellationToken).ConfigureAwait(false); } else { return false; } break; } } } else if (rlvMessage.Behavior.StartsWith("getdebug_", StringComparison.OrdinalIgnoreCase)) { var commandRaw = rlvMessage.Behavior.Substring("getdebug_".Length); var (success, debugInfo) = await _queryCallbacks.TryGetDebugSettingValueAsync(commandRaw, cancellationToken).ConfigureAwait(false); if (success) { response = debugInfo; } } else if (rlvMessage.Behavior.StartsWith("getenv_", StringComparison.OrdinalIgnoreCase)) { var commandRaw = rlvMessage.Behavior.Substring("getenv_".Length); var (success, envInfo) = await _queryCallbacks.TryGetEnvironmentSettingValueAsync(commandRaw, cancellationToken).ConfigureAwait(false); if (success) { response = envInfo; } } if (response != null) { await _actionCallbacks.SendReplyAsync(channel, response, cancellationToken).ConfigureAwait(false); return true; } return false; } private sealed class InvWornInfoContainer { public string FolderName { get; } public string CountIndicator { get; } public InvWornInfoContainer(string folderName, string countIndicator) { FolderName = folderName; CountIndicator = countIndicator; } public override string ToString() { return $"{FolderName}|{CountIndicator}"; } } private static void GetInvWornInfo_Internal(RlvSharedFolder folder, bool recursive, ref int totalItems, ref int totalItemsWorn) { totalItemsWorn += folder.Items.Count(n => n.AttachedTo != null || n.WornOn != null); totalItems += folder.Items.Count; if (recursive) { foreach (var child in folder.Children) { GetInvWornInfo_Internal(child, recursive, ref totalItems, ref totalItemsWorn); } } } private static string GetInvWornInfo(RlvSharedFolder folder) { // 0 : No item is present in that folder // 1 : Some items are present in that folder, but none of them is worn // 2 : Some items are present in that folder, and some of them are worn // 3 : Some items are present in that folder, and all of them are worn var totalItemsWorn = 0; var totalItems = 0; GetInvWornInfo_Internal(folder, false, ref totalItems, ref totalItemsWorn); var result = string.Empty; if (totalItems == 0) { result += "0"; } else if (totalItemsWorn == 0) { result += "1"; } else if (totalItems != totalItemsWorn) { result += "2"; } else { result += "3"; } var totalItemsWornRecursive = 0; var totalItemsRecursive = 0; GetInvWornInfo_Internal(folder, true, ref totalItemsRecursive, ref totalItemsWornRecursive); if (totalItemsRecursive == 0) { result += "0"; } else if (totalItemsWornRecursive == 0) { result += "1"; } else if (totalItemsRecursive != totalItemsWornRecursive) { result += "2"; } else { result += "3"; } return result; } private async Task HandleGetInvWorn(string args, CancellationToken cancellationToken) { var (hasSharedFolder, sharedFolder) = await _queryCallbacks.TryGetSharedFolderAsync(cancellationToken).ConfigureAwait(false); if (!hasSharedFolder || sharedFolder == null) { return string.Empty; } var inventoryMap = new InventoryMap(sharedFolder); var folders = new List(); var target = sharedFolder; if (args.Length != 0) { if (!inventoryMap.TryGetFolderFromPath(args, false, out target)) { return string.Empty; } } var resultItems = new List { new("", GetInvWornInfo(target)) }; var foldersInInv = target.Children .Where(n => !n.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase)); foreach (var folder in foldersInInv) { var invWornInfo = GetInvWornInfo(folder); resultItems.Add(new InvWornInfoContainer(folder.Name, invWornInfo)); } var result = string.Join(",", resultItems); return result; } private static void SearchFoldersForName(RlvSharedFolder root, bool stopOnFirstResult, IEnumerable searchTerms, List outFoundFolders) { if (searchTerms.All(root.Name.Contains)) { outFoundFolders.Add(root); if (stopOnFirstResult) { return; } } foreach (var child in root.Children) { if (child.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase) || child.Name.StartsWith("~", StringComparison.OrdinalIgnoreCase)) { continue; } SearchFoldersForName(child, stopOnFirstResult, searchTerms, outFoundFolders); if (stopOnFirstResult && outFoundFolders.Count > 0) { return; } } } private async Task HandleFindFolders(bool stopOnFirstResult, IEnumerable searchTerms, string separator, CancellationToken cancellationToken) { var (hasSharedFolder, sharedFolder) = await _queryCallbacks.TryGetSharedFolderAsync(cancellationToken).ConfigureAwait(false); if (!hasSharedFolder || sharedFolder == null) { return string.Empty; } var inventoryMap = new InventoryMap(sharedFolder); var foundFolders = new List(); SearchFoldersForName(sharedFolder, stopOnFirstResult, searchTerms, foundFolders); var foundFolderPaths = new List(); foreach (var folder in foundFolders) { if (inventoryMap.TryBuildPathToFolder(folder.Id, out var foundPath)) { foundFolderPaths.Add(foundPath); } } var result = string.Join(separator, foundFolderPaths); return result; } private async Task HandleGetInv(string args, CancellationToken cancellationToken) { var (hasSharedFolder, sharedFolder) = await _queryCallbacks.TryGetSharedFolderAsync(cancellationToken).ConfigureAwait(false); if (!hasSharedFolder || sharedFolder == null) { return string.Empty; } var inventoryMap = new InventoryMap(sharedFolder); var target = sharedFolder; if (args.Length != 0) { if (!inventoryMap.TryGetFolderFromPath(args, false, out target)) { return string.Empty; } } var foldersNamesInInv = target.Children .Where(n => !n.Name.StartsWith(".", StringComparison.OrdinalIgnoreCase)) .Select(n => n.Name); var result = string.Join(",", foldersNamesInInv); return result; } private async Task HandleGetPath(bool limitToOneResult, Guid? attachedPrimId, RlvAttachmentPoint? attachmentPoint, RlvWearableType? wearableType, CancellationToken cancellationToken) { var (hasSharedFolder, sharedFolder) = await _queryCallbacks.TryGetSharedFolderAsync(cancellationToken).ConfigureAwait(false); if (!hasSharedFolder || sharedFolder == null) { return string.Empty; } var inventoryMap = new InventoryMap(sharedFolder); var folders = inventoryMap.FindFoldersContaining(limitToOneResult, attachedPrimId, attachmentPoint, wearableType); var sb = new StringBuilder(); foreach (var folder in folders.OrderBy(n => n.Name)) { if (sb.Length > 0) { sb.Append(','); } if (inventoryMap.TryBuildPathToFolder(folder.Id, out var foundPath)) { sb.Append(foundPath); } } return sb.ToString(); } internal async Task ProcessInstantMessageCommand(string message, Guid senderId, CancellationToken cancellationToken) { switch (message) { case "@version": await _actionCallbacks.SendInstantMessageAsync(senderId, RlvService.RLVVersion, cancellationToken).ConfigureAwait(false); return true; case "@getblacklist": var blacklist = _blacklist.GetBlacklist(); await _actionCallbacks.SendInstantMessageAsync(senderId, string.Join(",", blacklist), cancellationToken).ConfigureAwait(false); return true; } return false; } } }