/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Reflection; using System.Threading; using OpenMetaverse; using OpenMetaverse.StructuredData; using log4net; using Nini.Config; using OpenSim.Framework; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; using OpenSim.Services.Interfaces; using Mono.Addins; using OpenSim.Services.Connectors.Hypergrid; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Services.UserProfilesService; using GridRegion = OpenSim.Services.Interfaces.GridRegion; using OpenSim.Region.CoreModules.Avatar.Friends; namespace OpenSim.Region.CoreModules.Avatar.UserProfiles { [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "UserProfilesModule")] public class UserProfileModule : IProfileModule, INonSharedRegionModule { const double PROFILECACHEEXPIRE = 300; /// /// Logging /// static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // The pair of Dictionaries are used to handle the switching of classified ads // by maintaining a cache of classified id to creator id mappings and an interest // count. The entries are removed when the interest count reaches 0. readonly Dictionary m_classifiedCache = new(); readonly Dictionary m_classifiedInterest = new(); readonly ExpiringCacheOS m_profilesCache = new(60000); IGroupsModule m_groupsModule = null; private readonly JsonRpcRequestManager rpc = new(); private bool m_allowUserProfileWebURLs = true; struct AsyncPropsRequest { public IClientAPI client; public ScenePresence presence; public UUID agent; public int reqtype; } private readonly ConcurrentStack m_asyncRequests = new(); private readonly object m_asyncRequestsLock = new(); private bool m_asyncRequestsRunning = false; private void ProcessRequests() { lock(m_asyncRequestsLock) { while (m_asyncRequests.TryPop(out AsyncPropsRequest req)) { try { IClientAPI client = req.client; if(!client.IsActive) continue; if(req.reqtype == 0) { ScenePresence p = req.presence; bool ok = true; bool foreign = GetUserProfileServerURI(req.agent, out string serverURI); if(serverURI.Length == 0) ok = false; Byte[] membershipType = new Byte[1]; string born = string.Empty; uint flags = 0x00; if (ok && GetUserAccountData(req.agent, out UserAccount acc)) { flags = (uint)(acc.UserFlags & 0xff); if (acc.UserTitle.Length == 0) membershipType[0] = (byte)((acc.UserFlags & 0x0f00) >> 8); else membershipType = Utils.StringToBytes(acc.UserTitle); int val_born = acc.Created; if (val_born != 0) born = Util.ToDateTime(val_born).ToString("M/d/yyyy", CultureInfo.InvariantCulture); } else ok = false; UserProfileProperties props = new() { UserId = req.agent }; if (ok) ok = GetProfileData(ref props, foreign, serverURI, out string result); if (!ok) props.AboutText = "Profile not available at this time. User may still be unknown to this grid"; if (!m_allowUserProfileWebURLs) props.WebUrl = ""; GroupMembershipData[] agentGroups = null; if(ok && m_groupsModule is not null) agentGroups = m_groupsModule.GetMembershipData(req.agent); HashSet clients; lock (m_profilesCache) { if (!m_profilesCache.TryGetValue(props.UserId, out UserProfileCacheEntry uce) || uce is null) uce = new UserProfileCacheEntry(); uce.props = props; uce.born = born; uce.membershipType = membershipType; uce.flags = flags; clients = uce.ClientsWaitingProps; uce.ClientsWaitingProps = null; uce.avatarGroups = agentGroups; m_profilesCache.AddOrUpdate(props.UserId, uce, PROFILECACHEEXPIRE); } if (IsFriendOnline(req.client, req.agent)) flags |= (uint)ProfileFlags.Online; else flags &= (uint)~ProfileFlags.Online; if (clients is null) { client.SendAvatarProperties(props.UserId, props.AboutText, born, membershipType, props.FirstLifeText, flags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); client.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); if (agentGroups is not null) client.SendAvatarGroupsReply(req.agent, agentGroups); } else { if (!clients.Contains(client) && client.IsActive) { client.SendAvatarProperties(props.UserId, props.AboutText, born, membershipType, props.FirstLifeText, flags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); client.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); if (agentGroups is not null) client.SendAvatarGroupsReply(req.agent, agentGroups); } foreach (IClientAPI cli in clients) { if (!cli.IsActive) continue; cli.SendAvatarProperties(props.UserId, props.AboutText, born, membershipType, props.FirstLifeText, flags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); cli.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); if (agentGroups is not null) cli.SendAvatarGroupsReply(req.agent, agentGroups); } } } } catch (Exception e) { m_log.ErrorFormat("[UserProfileModule]: Process fail {0} : {1}", e.Message, e.StackTrace); } } m_asyncRequestsRunning = false; } } public Scene Scene { get; private set; } /// /// Gets or sets the ConfigSource. /// /// /// The configuration /// public IConfigSource Config { get; set; } /// /// Gets or sets the URI to the profile server. /// /// /// The profile server URI. /// public string ProfileServerUri { get; set; } IProfileModule ProfileModule { get; set; } IUserManagement UserManagementModule { get; set; } /// /// Gets or sets a value indicating whether this /// is enabled. /// /// /// true if enabled; otherwise, false. /// public bool Enabled { get; set; } private GridInfo m_thisGridInfo; #region IRegionModuleBase implementation /// /// This is called to initialize the region module. For shared modules, this is called exactly once, after /// creating the single (shared) instance. For non-shared modules, this is called once on each instance, after /// the instace for the region has been created. /// /// /// Source. /// public void Initialise(IConfigSource source) { Config = source; ReplaceableInterface = typeof(IProfileModule); IConfig profileConfig = Config.Configs["UserProfiles"]; if (profileConfig is null) { //m_log.Debug("[PROFILES]: UserProfiles disabled, no configuration"); Enabled = false; return; } // If we find ProfileURL then we configure for FULL support // else we setup for BASIC support ProfileServerUri = profileConfig.GetString("ProfileServiceURL", ""); if (string.IsNullOrEmpty(ProfileServerUri)) { Enabled = false; return; } OSHTTPURI tmp = new(ProfileServerUri, true); if (!tmp.IsResolvedHost) { m_log.ErrorFormat("[UserProfileModule: {0}", tmp.IsValidHost ? "Could not resolve ProfileServiceURL" : "ProfileServiceURL is a invalid host"); throw new Exception("UserProfileModule init error"); } ProfileServerUri = tmp.URI; m_allowUserProfileWebURLs = profileConfig.GetBoolean("AllowUserProfileWebURLs", m_allowUserProfileWebURLs); m_log.Debug("[UserProfileModule]: Full Profiles Enabled"); ReplaceableInterface = null; Enabled = true; } /// /// Adds the region. /// /// /// Scene. /// public void AddRegion(Scene scene) { if(!Enabled) return; Scene = scene; m_thisGridInfo ??= scene.SceneGridInfo; Scene.RegisterModuleInterface(this); Scene.EventManager.OnNewClient += OnNewClient; Scene.EventManager.OnClientClosed += OnClientClosed; UserManagementModule = Scene.RequestModuleInterface(); } /// /// Removes the region. /// /// /// Scene. /// public void RemoveRegion(Scene scene) { if(!Enabled) return; m_profilesCache.Clear(); m_classifiedCache.Clear(); m_classifiedInterest.Clear(); } /// /// This will be called once for every scene loaded. In a shared module this will be multiple times in one /// instance, while a nonshared module instance will only be called once. This method is called after AddRegion /// has been called in all modules for that scene, providing an opportunity to request another module's /// interface, or hook an event from another module. /// /// /// Scene. /// public void RegionLoaded(Scene scene) { if(!Enabled) return; m_groupsModule = Scene.RequestModuleInterface(); } /// /// If this returns non-null, it is the type of an interface that this module intends to register. This will /// cause the loader to defer loading of this module until all other modules have been loaded. If no other /// module has registered the interface by then, this module will be activated, else it will remain inactive, /// letting the other module take over. This should return non-null ONLY in modules that are intended to be /// easily replaceable, e.g. stub implementations that the developer expects to be replaced by third party /// provided modules. /// /// /// The replaceable interface. /// public Type ReplaceableInterface { get; private set; } /// /// Called as the instance is closed. /// public void Close() { m_thisGridInfo = null; } /// /// The name of the module /// /// /// Gets the module name. /// public string Name { get { return "UserProfileModule"; } } #endregion IRegionModuleBase implementation #region Region Event Handlers /// /// Raises the new client event. /// /// /// Client. /// void OnNewClient(IClientAPI client) { //Profile client.OnRequestAvatarProperties += RequestAvatarProperties; client.OnUpdateAvatarProperties += AvatarPropertiesUpdate; client.OnAvatarInterestUpdate += AvatarInterestsUpdate; // Classifieds client.AddGenericPacketHandler("avatarclassifiedsrequest", ClassifiedsRequest); client.OnClassifiedInfoUpdate += ClassifiedInfoUpdate; client.OnClassifiedInfoRequest += ClassifiedInfoRequest; client.OnClassifiedDelete += ClassifiedDelete; // Picks client.AddGenericPacketHandler("avatarpicksrequest", PicksRequest); client.AddGenericPacketHandler("pickinforequest", PickInfoRequest); client.OnPickInfoUpdate += PickInfoUpdate; client.OnPickDelete += PickDelete; // Notes client.AddGenericPacketHandler("avatarnotesrequest", NotesRequest); client.OnAvatarNotesUpdate += NotesUpdate; // Preferences client.OnUserInfoRequest += UserPreferencesRequest; client.OnUpdateUserInfo += UpdateUserPreferences; } void OnClientClosed(UUID AgentId, Scene scene) { ScenePresence sp = scene.GetScenePresence(AgentId); IClientAPI client = sp.ControllingClient; if (client is null) return; //Profile client.OnRequestAvatarProperties -= RequestAvatarProperties; client.OnUpdateAvatarProperties -= AvatarPropertiesUpdate; client.OnAvatarInterestUpdate -= AvatarInterestsUpdate; // Classifieds client.OnClassifiedInfoUpdate -= ClassifiedInfoUpdate; client.OnClassifiedInfoRequest -= ClassifiedInfoRequest; client.OnClassifiedDelete -= ClassifiedDelete; // Picks client.OnPickInfoUpdate -= PickInfoUpdate; client.OnPickDelete -= PickDelete; // Notes client.OnAvatarNotesUpdate -= NotesUpdate; // Preferences client.OnUserInfoRequest -= UserPreferencesRequest; client.OnUpdateUserInfo -= UpdateUserPreferences; } #endregion Region Event Handlers #region Classified /// /// /// Handles the avatar classifieds request. /// /// /// Sender. /// /// /// Method. /// /// /// Arguments. /// public void ClassifiedsRequest(Object sender, string method, List args) { if (sender is not IClientAPI remoteClient) return; Dictionary classifieds = new(); if (!UUID.TryParse(args[0], out UUID targetID) || targetID.IsZero()) return; if (targetID.Equals(Constants.m_MrOpenSimID)) { remoteClient.SendAvatarClassifiedReply(targetID, classifieds); return; } ScenePresence p = FindPresence(targetID); if (p is not null && p.IsNPC) { remoteClient.SendAvatarClassifiedReply(targetID, classifieds); return; } lock(m_profilesCache) { if(m_profilesCache.TryGetValue(targetID, out UserProfileCacheEntry uce) && uce is not null) { if(uce.classifiedsLists is not null) { foreach(KeyValuePair kvp in uce.classifiedsLists) { UUID kvpkey = kvp.Key; classifieds[kvpkey] = kvp.Value; lock (m_classifiedCache) { if (!m_classifiedCache.ContainsKey(kvpkey)) { m_classifiedCache.Add(kvpkey,targetID); m_classifiedInterest.Add(kvpkey, 0); } m_classifiedInterest[kvpkey]++; } } remoteClient.SendAvatarClassifiedReply(targetID, uce.classifiedsLists); return; } } } GetUserProfileServerURI(targetID, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { remoteClient.SendAvatarClassifiedReply(targetID, classifieds); return; } OSDMap parameters= new() { {"creatorId", OSD.FromUUID(targetID)} }; OSD osdtmp = parameters; if(!rpc.JsonRpcRequest(ref osdtmp, "avatarclassifiedsrequest", serverURI, UUID.Random().ToString())) { remoteClient.SendAvatarClassifiedReply(targetID, classifieds); return; } parameters = (OSDMap)osdtmp; if(!parameters.TryGetValue("result", out osdtmp) || osdtmp is not OSDArray) { remoteClient.SendAvatarClassifiedReply(targetID, classifieds); return; } OSDArray list = (OSDArray)osdtmp; foreach(OSD map in list) { OSDMap m = (OSDMap)map; UUID cid = m["classifieduuid"].AsUUID(); string name = m["name"].AsString(); classifieds[cid] = name; lock (m_classifiedCache) { if (!m_classifiedCache.ContainsKey(cid)) { m_classifiedCache.Add(cid,targetID); m_classifiedInterest.Add(cid, 0); } m_classifiedInterest[cid]++; } } lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(targetID, out UserProfileCacheEntry uce) || uce is null) uce = new UserProfileCacheEntry(); uce.classifiedsLists = classifieds; m_profilesCache.AddOrUpdate(targetID, uce, PROFILECACHEEXPIRE); } remoteClient.SendAvatarClassifiedReply(targetID, classifieds); } public void ClassifiedInfoRequest(UUID queryClassifiedID, IClientAPI remoteClient) { UUID target = remoteClient.AgentId; UserClassifiedAdd ad = new() { ClassifiedId = queryClassifiedID }; lock (m_classifiedCache) { if (m_classifiedCache.ContainsKey(queryClassifiedID)) { target = m_classifiedCache[queryClassifiedID]; m_classifiedInterest[queryClassifiedID]--; if (m_classifiedInterest[queryClassifiedID] == 0) { m_classifiedInterest.Remove(queryClassifiedID); m_classifiedCache.Remove(queryClassifiedID); } } } UserProfileCacheEntry uce = null; lock(m_profilesCache) { if(m_profilesCache.TryGetValue(target, out uce) && uce is not null) { if(uce.classifieds is not null && uce.classifieds.ContainsKey(queryClassifiedID)) { ad = uce.classifieds[queryClassifiedID]; if(Vector3.TryParse(ad.GlobalPos, out Vector3 gPos)) { remoteClient.SendClassifiedInfoReply(ad.ClassifiedId, ad.CreatorId, (uint)ad.CreationDate, (uint)ad.ExpirationDate, (uint)ad.Category, ad.Name, ad.Description, ad.ParcelId, (uint)ad.ParentEstate, ad.SnapshotId, ad.SimName, gPos, ad.ParcelName, ad.Flags, ad.Price); } return; } } } bool foreign = GetUserProfileServerURI(target, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { return; } object Adobject = ad; if(!rpc.JsonRpcRequest(ref Adobject, "classifieds_info_query", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage( "Error getting classified info", false); return; } ad = (UserClassifiedAdd) Adobject; if(ad.CreatorId.IsZero()) return; if(foreign) cacheForeignImage(target, ad.SnapshotId); lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(target, out uce) || uce is null) uce = new UserProfileCacheEntry(); uce.classifieds ??= new Dictionary(); uce.classifieds[ad.ClassifiedId] = ad; m_profilesCache.AddOrUpdate(target, uce, PROFILECACHEEXPIRE); } if(Vector3.TryParse(ad.GlobalPos, out Vector3 globalPos)) remoteClient.SendClassifiedInfoReply(ad.ClassifiedId, ad.CreatorId, (uint)ad.CreationDate, (uint)ad.ExpirationDate, (uint)ad.Category, ad.Name, ad.Description, ad.ParcelId, (uint)ad.ParentEstate, ad.SnapshotId, ad.SimName, globalPos, ad.ParcelName, ad.Flags, ad.Price); } /// /// Classifieds info update. /// /// /// Queryclassified I. /// /// /// Query category. /// /// /// Query name. /// /// /// Query description. /// /// /// Query parcel I. /// /// /// Query parent estate. /// /// /// Query snapshot I. /// /// /// Query global position. /// /// /// Queryclassified flags. /// /// /// Queryclassified price. /// /// /// Remote client. /// public void ClassifiedInfoUpdate(UUID queryclassifiedID, uint queryCategory, string queryName, string queryDescription, UUID queryParcelID, uint queryParentEstate, UUID querySnapshotID, Vector3 queryGlobalPos, byte queryclassifiedFlags, int queryclassifiedPrice, IClientAPI remoteClient) { Scene s = (Scene)remoteClient.Scene; Vector3 pos = remoteClient.SceneAgent.AbsolutePosition; ILandObject land = s.LandChannel.GetLandObject(pos.X, pos.Y); UUID creatorId = remoteClient.AgentId; ScenePresence p = FindPresence(creatorId); UserProfileCacheEntry uce; lock (m_profilesCache) m_profilesCache.TryGetValue(remoteClient.AgentId, out uce); bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; if(foreign) { remoteClient.SendAgentAlertMessage("Please change classifieds on your home grid", true); if(uce is not null && uce.classifiedsLists is not null) remoteClient.SendAvatarClassifiedReply(remoteClient.AgentId, uce.classifiedsLists); return; } OSDMap parameters = new () { {"creatorId", OSD.FromUUID(creatorId)} }; OSD osdtmp = parameters; if (!rpc.JsonRpcRequest(ref osdtmp, "avatarclassifiedsrequest", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage("Error fetching classifieds", false); return; } parameters = (OSDMap)osdtmp; OSDArray list = (OSDArray)parameters["result"]; bool exists = list.Cast().Where(map => map.ContainsKey("classifieduuid")) .Any(map => map["classifieduuid"].AsUUID().Equals(queryclassifiedID)); IMoneyModule money = null; if (!exists) { money = s.RequestModuleInterface(); if (money is not null) { if (!money.AmountCovered(remoteClient.AgentId, queryclassifiedPrice)) { remoteClient.SendAgentAlertMessage("You do not have enough money to create this classified.", false); if(uce is not null && uce.classifiedsLists is not null) remoteClient.SendAvatarClassifiedReply(remoteClient.AgentId, uce.classifiedsLists); return; } } } UserClassifiedAdd ad = new() { ParcelName = land == null ? string.Empty : land.LandData.Name, CreatorId = remoteClient.AgentId, ClassifiedId = queryclassifiedID, Category = Convert.ToInt32(queryCategory), Name = queryName, Description = queryDescription, ParentEstate = Convert.ToInt32(queryParentEstate), SnapshotId = querySnapshotID, SimName = remoteClient.Scene.RegionInfo.RegionName, GlobalPos = queryGlobalPos.ToString(), Flags = queryclassifiedFlags, Price = queryclassifiedPrice, ParcelId = p.currentParcelUUID }; object Ad = ad; if(!rpc.JsonRpcRequest(ref Ad, "classified_update", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage("Error updating classified", false); if(uce is not null && uce.classifiedsLists is not null) remoteClient.SendAvatarClassifiedReply(remoteClient.AgentId, uce.classifiedsLists); return; } // only charge if it worked money?.ApplyCharge(remoteClient.AgentId, queryclassifiedPrice, MoneyTransactionType.ClassifiedCharge); // just flush cache for now lock(m_profilesCache) { if(m_profilesCache.TryGetValue(remoteClient.AgentId, out uce) && uce is not null) { uce.classifieds = null; uce.classifiedsLists = null; } } } /// /// Classifieds delete. /// /// /// Query classified I. /// /// /// Remote client. /// public void ClassifiedDelete(UUID queryClassifiedID, IClientAPI remoteClient) { bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; if(foreign) { remoteClient.SendAgentAlertMessage("Please change classifieds on your home grid", true); return; } if (!UUID.TryParse(queryClassifiedID.ToString(), out UUID classifiedId)) return; OSD Params = new OSDMap() {{ "classifiedId", OSD.FromUUID(classifiedId) }}; if(!rpc.JsonRpcRequest(ref Params, "classified_delete", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage( "Error deleting classified", false); return; } // flush cache lock(m_profilesCache) { if(m_profilesCache.TryGetValue(remoteClient.AgentId, out UserProfileCacheEntry uce) && uce is not null) { uce.classifieds = null; uce.classifiedsLists = null; } } } #endregion Classified #region Picks /// /// Handles the avatar picks request. /// /// /// Sender. /// /// /// Method. /// /// /// Arguments. /// public void PicksRequest(Object sender, string method, List args) { if (sender is not IClientAPI remoteClient) return; if(!UUID.TryParse(args[0], out UUID targetId)) return; Dictionary picks = new(); if (targetId.Equals(Constants.m_MrOpenSimID)) { remoteClient.SendAvatarPicksReply(targetId, picks); return; } ScenePresence p = FindPresence(targetId); if (p is not null && p.IsNPC) { remoteClient.SendAvatarPicksReply(targetId, picks); return; } UserProfileCacheEntry uce = null; lock(m_profilesCache) { if(m_profilesCache.TryGetValue(targetId, out uce) && uce is not null) { if(uce.picksList is not null) { remoteClient.SendAvatarPicksReply(targetId, uce.picksList); return; } } } GetUserProfileServerURI(targetId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { remoteClient.SendAvatarPicksReply(targetId, picks); return; } OSDMap parameters= new() { {"creatorId", OSD.FromUUID(targetId)} }; OSD osdtmp = parameters; if(!rpc.JsonRpcRequest(ref osdtmp, "avatarpicksrequest", serverURI, UUID.Random().ToString())) { remoteClient.SendAvatarPicksReply(targetId, picks); return; } parameters = (OSDMap)osdtmp; if(!parameters.TryGetValue("result", out osdtmp) || osdtmp is not OSDArray) { remoteClient.SendAvatarPicksReply(targetId, picks); return; } OSDArray list = (OSDArray)osdtmp; foreach(OSD map in list) { OSDMap m = (OSDMap)map; UUID cid = m["pickuuid"].AsUUID(); string name = m["name"].AsString(); picks[cid] = name; } lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(targetId, out uce) || uce is null) uce = new UserProfileCacheEntry(); uce.picksList = picks; m_profilesCache.AddOrUpdate(targetId, uce, PROFILECACHEEXPIRE); } remoteClient.SendAvatarPicksReply(targetId, picks); } /// /// Handles the pick info request. /// /// /// Sender. /// /// /// Method. /// /// /// Arguments. /// public void PickInfoRequest(Object sender, string method, List args) { if (sender is not IClientAPI) return; UserProfilePick pick = new(); if(!UUID.TryParse(args [0], out UUID targetID)) return; pick.CreatorId = targetID; if(!UUID.TryParse (args [1], out pick.PickId)) return; IClientAPI remoteClient = (IClientAPI)sender; UserProfileCacheEntry uce = null; lock(m_profilesCache) { if(m_profilesCache.TryGetValue(targetID, out uce) && uce is not null) { if(uce.picks is not null && uce.picks.ContainsKey(pick.PickId)) { pick = uce.picks[pick.PickId]; if(Vector3.TryParse(pick.GlobalPos, out Vector3 gPos)) remoteClient.SendPickInfoReply(pick.PickId,pick.CreatorId,pick.TopPick,pick.ParcelId,pick.Name, pick.Desc,pick.SnapshotId,pick.ParcelName,pick.OriginalName,pick.SimName, gPos,pick.SortOrder,pick.Enabled); return; } } } bool foreign = GetUserProfileServerURI (targetID, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { return; } object Pick = (object)pick; if (!rpc.JsonRpcRequest (ref Pick, "pickinforequest", serverURI, UUID.Random ().ToString ())) { remoteClient.SendAgentAlertMessage ("Error selecting pick", false); return; } pick = (UserProfilePick)Pick; if(foreign) cacheForeignImage(targetID, pick.SnapshotId); if(!Vector3.TryParse(pick.GlobalPos, out Vector3 globalPos)) return; if (m_thisGridInfo.IsLocalGrid(pick.Gatekeeper, true) == 0) { // Setup the illusion string region = string.Format("{0} {1}",pick.Gatekeeper,pick.SimName); GridRegion target = Scene.GridService.GetRegionByName(Scene.RegionInfo.ScopeID, region); if(target is null) { // This is a unreachable region } else { // we have a proxy on map if (Util.ParseFakeParcelID(pick.ParcelId, out ulong _, out uint oriX, out uint oriY)) { pick.ParcelId = Util.BuildFakeParcelID(target.RegionHandle, oriX, oriY); globalPos.X = target.RegionLocX + oriX; globalPos.Y = target.RegionLocY + oriY; pick.GlobalPos = globalPos.ToString(); } else { // this is a fail on large regions uint gtmp = (uint)globalPos.X >> 8; globalPos.X -= (gtmp << 8); gtmp = (uint)globalPos.Y >> 8; globalPos.Y -= (gtmp << 8); pick.ParcelId = Util.BuildFakeParcelID(target.RegionHandle, (uint)globalPos.X, (uint)globalPos.Y); globalPos.X += target.RegionLocX; globalPos.Y += target.RegionLocY; pick.GlobalPos = globalPos.ToString(); } } } //m_log.DebugFormat("[PROFILES]: PickInfoRequest: {0} : {1}", pick.Name.ToString(), pick.SnapshotId.ToString()); lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(targetID, out uce) || uce is null) uce = new UserProfileCacheEntry(); uce.picks ??= new Dictionary(); uce.picks[pick.PickId] = pick; m_profilesCache.AddOrUpdate(targetID, uce, PROFILECACHEEXPIRE); } // Pull the rabbit out of the hat remoteClient.SendPickInfoReply(pick.PickId,pick.CreatorId,pick.TopPick,pick.ParcelId,pick.Name, pick.Desc,pick.SnapshotId,pick.ParcelName,pick.OriginalName,pick.SimName, globalPos,pick.SortOrder,pick.Enabled); } /// /// Updates the userpicks /// /// /// Remote client. /// /// /// Pick I. /// /// /// the creator of the pick /// /// /// Top pick. /// /// /// Name. /// /// /// Desc. /// /// /// Snapshot I. /// /// /// Sort order. /// /// /// Enabled. /// public void PickInfoUpdate(IClientAPI remoteClient, UUID pickID, UUID creatorID, bool topPick, string name, string desc, UUID snapshotID, int sortOrder, bool enabled) { //m_log.DebugFormat("[PROFILES]: Start PickInfoUpdate Name: {0} PickId: {1} SnapshotId: {2}", name, pickID.ToString(), snapshotID.ToString()); UserProfilePick pick = new(); GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; ScenePresence p = FindPresence(remoteClient.AgentId); Vector3 avaPos = p.AbsolutePosition; // Getting the global position for the Avatar Vector3 posGlobal = new(remoteClient.Scene.RegionInfo.WorldLocX + avaPos.X, remoteClient.Scene.RegionInfo.WorldLocY + avaPos.Y, avaPos.Z); string landParcelName = "My Parcel"; // to locate parcels we use a fake id that encodes the region handle // since we do not have a global locator // this fails on HG UUID landParcelID = Util.BuildFakeParcelID(remoteClient.Scene.RegionInfo.RegionHandle, (uint)avaPos.X, (uint)avaPos.Y); ILandObject land = p.Scene.LandChannel.GetLandObject(avaPos.X, avaPos.Y); if (land is not null) { // If land found, use parcel uuid from here because the value from SP will be blank if the avatar hasnt moved landParcelName = land.LandData.Name; } else { m_log.WarnFormat( "[PROFILES]: PickInfoUpdate found no parcel info at {0},{1} in {2}", avaPos.X, avaPos.Y, p.Scene.Name); } pick.PickId = pickID; pick.CreatorId = creatorID; pick.TopPick = topPick; pick.Name = name; pick.Desc = desc; pick.ParcelId = landParcelID; pick.SnapshotId = snapshotID; pick.ParcelName = landParcelName; pick.SimName = remoteClient.Scene.RegionInfo.RegionName; pick.Gatekeeper = m_thisGridInfo.GateKeeperURLNoEndSlash; pick.GlobalPos = posGlobal.ToString(); pick.SortOrder = sortOrder; pick.Enabled = enabled; object Pick = (object)pick; if(!rpc.JsonRpcRequest(ref Pick, "picks_update", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage( "Error updating pick", false); return; } UserProfileCacheEntry uce = null; lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(remoteClient.AgentId, out uce) || uce is null) uce = new UserProfileCacheEntry(); uce.picks ??= new Dictionary(); uce.picksList ??= new Dictionary(); uce.picks[pick.PickId] = pick; uce.picksList[pick.PickId] = pick.Name; m_profilesCache.AddOrUpdate(remoteClient.AgentId, uce, PROFILECACHEEXPIRE); } remoteClient.SendAvatarPicksReply(remoteClient.AgentId, uce.picksList); remoteClient.SendPickInfoReply(pick.PickId,pick.CreatorId,pick.TopPick,pick.ParcelId,pick.Name, pick.Desc,pick.SnapshotId,pick.ParcelName,pick.OriginalName,pick.SimName, posGlobal,pick.SortOrder,pick.Enabled); //m_log.DebugFormat("[PROFILES]: Finish PickInfoUpdate {0} {1}", pick.Name, pick.PickId.ToString()); } /// /// Delete a Pick /// /// /// Remote client. /// /// /// Query pick I. /// public void PickDelete(IClientAPI remoteClient, UUID queryPickID) { GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { return; } OSDMap parameters = new() { { "pickId", OSD.FromUUID(queryPickID) } }; OSD Params = (OSD)parameters; if(!rpc.JsonRpcRequest(ref Params, "picks_delete", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage( "Error picks delete", false); return; } UserProfileCacheEntry uce = null; lock(m_profilesCache) { if(m_profilesCache.TryGetValue(remoteClient.AgentId, out uce) && uce is not null) { uce.picks?.Remove(queryPickID); uce.picksList?.Remove(queryPickID); m_profilesCache.AddOrUpdate(remoteClient.AgentId, uce, PROFILECACHEEXPIRE); } } if(uce is not null && uce.picksList is not null) remoteClient.SendAvatarPicksReply(remoteClient.AgentId, uce.picksList); else remoteClient.SendAvatarPicksReply(remoteClient.AgentId, new Dictionary()); } #endregion Picks #region Notes /// /// Handles the avatar notes request. /// /// /// Sender. /// /// /// Method. /// /// /// Arguments. /// public void NotesRequest(Object sender, string method, List args) { if (sender is not IClientAPI remoteClient) return; UserProfileNotes note = new(); if (!UUID.TryParse(args[0], out note.TargetId)) return; note.UserId = remoteClient.AgentId; GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) { remoteClient.SendAvatarNotesReply(note.TargetId, note.Notes); return; } object Note = (object)note; if(!rpc.JsonRpcRequest(ref Note, "avatarnotesrequest", serverURI, UUID.Random().ToString())) { remoteClient.SendAvatarNotesReply(note.TargetId, note.Notes); return; } note = (UserProfileNotes) Note; remoteClient.SendAvatarNotesReply(note.TargetId, note.Notes); } /// /// Avatars the notes update. /// /// /// Remote client. /// /// /// Query target I. /// /// /// Query notes. /// public void NotesUpdate(IClientAPI remoteClient, UUID queryTargetID, string queryNotes) { if (queryTargetID.Equals(Constants.m_MrOpenSimID)) return; ScenePresence p = FindPresence(queryTargetID); if (p is not null && p.IsNPC) { remoteClient.SendAgentAlertMessage("Notes for NPCs not available", false); return; } UserProfileNotes note = new() { UserId = remoteClient.AgentId, TargetId = queryTargetID, Notes = queryNotes }; GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; object Note = note; if(!rpc.JsonRpcRequest(ref Note, "avatar_notes_update", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage( "Error updating note", false); return; } } #endregion Notes #region User Preferences /// /// Updates the user preferences. /// /// /// Im via email. /// /// /// Visible. /// /// /// Remote client. /// public void UpdateUserPreferences(bool imViaEmail, bool visible, IClientAPI remoteClient) { UserPreferences pref = new() { UserId = remoteClient.AgentId, IMViaEmail = imViaEmail, Visible = visible }; _ = GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; object Pref = pref; if(!rpc.JsonRpcRequest(ref Pref, "user_preferences_update", serverURI, UUID.Random().ToString())) { m_log.InfoFormat("[PROFILES]: UserPreferences update error"); remoteClient.SendAgentAlertMessage("Error updating preferences", false); return; } } /// /// Users the preferences request. /// /// /// Remote client. /// public void UserPreferencesRequest(IClientAPI remoteClient) { GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if(string.IsNullOrWhiteSpace(serverURI)) return; UserPreferences pref = new() { UserId = remoteClient.AgentId }; object Pref = (object)pref; if(!rpc.JsonRpcRequest(ref Pref, "user_preferences_request", serverURI, UUID.Random().ToString())) { //m_log.InfoFormat("[PROFILES]: UserPreferences request error"); //remoteClient.SendAgentAlertMessage("Error requesting preferences", false); return; } pref = (UserPreferences) Pref; remoteClient.SendUserInfoReply(pref.IMViaEmail, pref.Visible, pref.EMail); } #endregion User Preferences #region Avatar Properties /// /// Update the avatars interests . /// /// /// Remote client. /// /// /// Wantmask. /// /// /// Wanttext. /// /// /// Skillsmask. /// /// /// Skillstext. /// /// /// Languages. /// public void AvatarInterestsUpdate(IClientAPI remoteClient, uint wantmask, string wanttext, uint skillsmask, string skillstext, string languages) { GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if (string.IsNullOrWhiteSpace(serverURI)) return; object Param = new UserProfileProperties() { UserId = remoteClient.AgentId, WantToMask = (int)wantmask, WantToText = wanttext, SkillsMask = (int)skillsmask, SkillsText = skillstext, Language = languages }; if(!rpc.JsonRpcRequest(ref Param, "avatar_interests_update", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage("Error updating interests", false); return; } // flush cache lock(m_profilesCache) { if(m_profilesCache.TryGetValue(remoteClient.AgentId, out UserProfileCacheEntry uce) && uce is not null) { uce.props = null; uce.ClientsWaitingProps = null; } } RequestAvatarProperties(remoteClient, remoteClient.AgentId); } public void RequestAvatarProperties(IClientAPI remoteClient, UUID avatarID) { if (avatarID.IsZero()) { // Looking for a reason that some viewers are sending null Id's m_log.Debug("[PROFILES]: got request of null ID"); return; } if (avatarID.Equals(Constants.m_MrOpenSimID)) { remoteClient.SendAvatarProperties(avatarID, "Creator of OpenSimulator shared assets library", Constants.m_MrOpenSimBorn.ToString(), Utils.StringToBytes("System agent"), "MrOpenSim has no life", 0x10, UUID.Zero, UUID.Zero, "", UUID.Zero); remoteClient.SendAvatarInterestsReply(avatarID, 0, "", 0, "Getting into trouble", "Droidspeak"); return; } ScenePresence p = FindPresence(avatarID); if (p is not null && p.IsNPC) { remoteClient.SendAvatarProperties(avatarID, ((INPC)(p.ControllingClient)).profileAbout, ((INPC)(p.ControllingClient)).Born, Utils.StringToBytes("Non Player Character (NPC)"), "NPCs have no life", 0x10, UUID.Zero, ((INPC)(p.ControllingClient)).profileImage, "", UUID.Zero); remoteClient.SendAvatarInterestsReply(avatarID, 0, "", 0, "Getting into trouble", "Droidspeak"); return; } UserProfileProperties props; lock(m_profilesCache) { if(m_profilesCache.TryGetValue(avatarID, out UserProfileCacheEntry uce) && uce is not null) { if(uce.props is not null) { props = uce.props; uint cflags = uce.flags; if (IsFriendOnline(remoteClient, avatarID)) cflags = (uint)ProfileFlags.Online; else cflags &= (uint)~ProfileFlags.Online; remoteClient.SendAvatarProperties(props.UserId, props.AboutText, uce.born, uce.membershipType , props.FirstLifeText, cflags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); remoteClient.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); if(uce.avatarGroups is not null) remoteClient.SendAvatarGroupsReply(avatarID, uce.avatarGroups); return; } else { if(uce.ClientsWaitingProps == null) uce.ClientsWaitingProps = new HashSet(); else if(uce.ClientsWaitingProps.Contains(remoteClient)) return; uce.ClientsWaitingProps.Add(remoteClient); } } else { uce = new UserProfileCacheEntry { ClientsWaitingProps = new HashSet() }; uce.ClientsWaitingProps.Add(remoteClient); m_profilesCache.AddOrUpdate(avatarID, uce, PROFILECACHEEXPIRE); } } AsyncPropsRequest req = new() { client = remoteClient, presence = p, agent = avatarID, reqtype = 0 }; m_asyncRequests.Push(req); if (Monitor.TryEnter(m_asyncRequestsLock)) { if (!m_asyncRequestsRunning) { m_asyncRequestsRunning = true; Util.FireAndForget(x => ProcessRequests()); } Monitor.Exit(m_asyncRequestsLock); } /* string serverURI = string.Empty; bool foreign = GetUserProfileServerURI(avatarID, out serverURI); UserAccount account = null; Dictionary userInfo; if (!foreign) { account = Scene.UserAccountService.GetUserAccount(Scene.RegionInfo.ScopeID, avatarID); } else { userInfo = new Dictionary(); } Byte[] membershipType = new Byte[1]; string born = string.Empty; uint flags = 0x00; if (null != account) { if (account.UserTitle.Length == 0) membershipType[0] = (Byte)((account.UserFlags & 0xf00) >> 8); else membershipType = Utils.StringToBytes(account.UserTitle); born = Util.ToDateTime(account.Created).ToString( "M/d/yyyy", CultureInfo.InvariantCulture); flags = (uint)(account.UserFlags & 0xff); } else { if (GetUserAccountData(avatarID, out userInfo) == true) { if ((string)userInfo["user_title"].Length == 0) membershipType[0] = (Byte)(((Byte)userInfo["user_flags"] & 0xf00) >> 8); else membershipType = Utils.StringToBytes((string)userInfo["user_title"]); int val_born = (int)userInfo["user_created"]; if(val_born != 0) born = Util.ToDateTime(val_born).ToString( "M/d/yyyy", CultureInfo.InvariantCulture); // picky, picky int val_flags = (int)userInfo["user_flags"]; flags = (uint)(val_flags & 0xff); } } props = new UserProfileProperties(); props.UserId = avatarID; string result = string.Empty; if(!GetProfileData(ref props, foreign, serverURI, out result)) { props.AboutText ="Profile not available at this time. User may still be unknown to this grid"; } if(!m_allowUserProfileWebURLs) props.WebUrl =""; HashSet clients; lock(m_profilesCache) { if(!m_profilesCache.TryGetValue(props.UserId, out uce) || uce == null) uce = new UserProfileCacheEntry(); uce.props = props; uce.born = born; uce.membershipType = membershipType; uce.flags = flags; clients = uce.ClientsWaitingProps; uce.ClientsWaitingProps = null; m_profilesCache.AddOrUpdate(props.UserId, uce, PROFILECACHEEXPIRE); } // if on same region force online if(p != null && !p.IsDeleted) flags |= 0x10; if(clients == null) { remoteClient.SendAvatarProperties(props.UserId, props.AboutText, born, membershipType , props.FirstLifeText, flags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); remoteClient.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); } else { if(!clients.Contains(remoteClient)) clients.Add(remoteClient); foreach(IClientAPI cli in clients) { if(!cli.IsActive) continue; cli.SendAvatarProperties(props.UserId, props.AboutText, born, membershipType , props.FirstLifeText, flags, props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); cli.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, props.SkillsText, props.Language); } } */ } /// /// Updates the avatar properties. /// /// /// Remote client. /// /// /// New profile. /// public void AvatarPropertiesUpdate(IClientAPI remoteClient, UserProfileProperties newProfile) { GetUserProfileServerURI(remoteClient.AgentId, out string serverURI); if (string.IsNullOrWhiteSpace(serverURI)) return; if (!m_allowUserProfileWebURLs) newProfile.WebUrl = string.Empty; object Prop = newProfile; if(!rpc.JsonRpcRequest(ref Prop, "avatar_properties_update", serverURI, UUID.Random().ToString())) { remoteClient.SendAgentAlertMessage("Error updating properties", false); return; } // flush cache lock(m_profilesCache) { if(m_profilesCache.TryGetValue(remoteClient.AgentId, out UserProfileCacheEntry uce) && uce is not null) { uce.props = null; uce.ClientsWaitingProps = null; } } RequestAvatarProperties(remoteClient, remoteClient.AgentId); } /// /// Gets the profile data. /// /// /// The profile data. /// bool GetProfileData(ref UserProfileProperties properties, bool foreign, string serverURI, out string message) { if (String.IsNullOrEmpty(serverURI)) { message = "User profile service unknown at this time"; return false; } object Prop = properties; if (!rpc.JsonRpcRequest(ref Prop, "avatar_properties_request", serverURI, UUID.Random().ToString())) { // If it's a foreign user then try again using OpenProfile, in case that's what the grid is using bool secondChanceSuccess = false; if (foreign) { try { OpenProfileClient client = new(serverURI); if (client.RequestAvatarPropertiesUsingOpenProfile(ref properties)) secondChanceSuccess = true; } catch (Exception e) { m_log.Debug( $"[PROFILES]: Request using the OpenProfile API for user {properties.UserId} to {serverURI} failed: {e.Message}"); // Allow the return 'message' to say "JsonRpcRequest" and not "OpenProfile", because // the most likely reason that OpenProfile failed is that the remote server // doesn't support OpenProfile, and that's not very interesting. } } if (!secondChanceSuccess) { message = $"JsonRpcRequest for user {properties.UserId} to {serverURI} failed"; m_log.Debug($"[PROFILES]: {message}"); return false; } } properties = (UserProfileProperties)Prop; if(foreign) { cacheForeignImage(properties.UserId, properties.ImageId); cacheForeignImage(properties.UserId, properties.FirstLifeImageId); } message = "Success"; return true; } #endregion Avatar Properties #region Utils /// /// Gets the user account data. /// /// /// The user profile data. /// /// /// If set to true user I. /// /// /// If set to true user info. /// bool GetUserAccountData(UUID userID, out UserAccount account) { account = null; if (UserManagementModule.IsLocalGridUser(userID)) { // Is local IUserAccountService uas = Scene.UserAccountService; account = uas.GetUserAccount(Scene.RegionInfo.ScopeID, userID); return account is not null; } else { // Is Foreign string home_url = UserManagementModule.GetUserServerURL(userID, "HomeURI", out bool recentFailedWeb); if (recentFailedWeb || String.IsNullOrEmpty(home_url)) return false; UserAgentServiceConnector uConn = new(home_url); Dictionary info; try { info = uConn.GetUserInfo(userID); } catch (Exception e) { m_log.Debug("[PROFILES]: GetUserInfo call failed ", e); UserManagementModule.UserWebFailed(userID); return false; } if (info.Count == 0) return false; account = new UserAccount(); if (info.ContainsKey("user_flags")) account.UserFlags = (int)info["user_flags"]; if (info.ContainsKey("user_created")) account.Created = (int)info["user_created"]; account.UserTitle = "HG Visitor"; return true; } } /// /// Gets the user profile server UR. /// /// /// The user profile server UR. /// /// /// If set to true user I. /// /// /// If set to true server UR. /// bool GetUserProfileServerURI(UUID userID, out string serverURI) { if (!UserManagementModule.IsLocalGridUser(userID)) { serverURI = UserManagementModule.GetUserServerURL(userID, "ProfileServerURI", out bool failed); if(failed) serverURI = string.Empty; // Is Foreign return true; } else { serverURI = ProfileServerUri; // Is local return false; } } void cacheForeignImage(UUID agent, UUID imageID) { if(imageID.IsZero()) return; string assetServerURI = UserManagementModule.GetUserServerURL(agent, "AssetServerURI"); if(string.IsNullOrWhiteSpace(assetServerURI)) return; Scene.AssetService.Get(imageID.ToString(), assetServerURI, false); } /// /// Finds the presence. /// /// /// The presence. /// /// /// Client I. /// ScenePresence FindPresence(UUID clientID) { ScenePresence p = Scene.GetScenePresence(clientID); if (p is not null && !p.IsChildAgent) return p; return null; } public virtual bool IsFriendOnline(IClientAPI client, UUID agent) { // if on same region force online ScenePresence p = Scene.GetScenePresence(agent); if (p is not null && !p.IsChildAgent && !p.IsDeleted) return true; IFriendsModule friendsModule = Scene.RequestModuleInterface(); if (friendsModule is not null && friendsModule.IsFriendOnline(client.AgentId, agent)) return true; if(client.SceneAgent is ScenePresence sp && sp.IsViewerUIGod) { Services.Interfaces.PresenceInfo[] pi = Scene.PresenceService?.GetAgents(new string[] { agent.ToString() }); return pi is not null && pi.Length > 0; } return false; } #endregion Util #region Web Util /// /// Sends json-rpc request with a serializable type. /// /// /// OSD Map. /// /// /// Serializable type . /// /// /// Json-rpc method to call. /// /// /// URI of json-rpc service. /// /// /// Id for our call. /// bool JsonRpcRequest(ref object parameters, string method, string uri, string jsonId) { return rpc?.JsonRpcRequest(ref parameters, method, uri, jsonId) ?? false; } /// /// Sends json-rpc request with OSD parameter. /// /// /// The rpc request. /// /// /// data - incoming as parameters, outgong as result/error /// /// /// Json-rpc method to call. /// /// /// URI of json-rpc service. /// /// /// If set to true json identifier. /// bool JsonRpcRequest(ref OSD data, string method, string uri, string jsonId) { return rpc?.JsonRpcRequest(ref data, method, uri, jsonId) ?? false; } #endregion Web Util } }