Update SecondBot services, commands (Info, Inventory, Interaction), and DataStore enhancements
Some checks are pending
autobuild / docker (push) Waiting to run
Some checks are pending
autobuild / docker (push) Waiting to run
This commit is contained in:
parent
f9094f6fca
commit
0b081d9e1f
16 changed files with 2624 additions and 90 deletions
|
|
@ -1334,7 +1334,7 @@ namespace OpenMetaverse
|
||||||
|
|
||||||
IPEndPoint endPoint = new IPEndPoint(ip, port);
|
IPEndPoint endPoint = new IPEndPoint(ip, port);
|
||||||
|
|
||||||
if (FindSimulator(endPoint) != null) return;
|
if (FindSimulator(endPoint) != null) continue;
|
||||||
|
|
||||||
if (Connect(ip, port, handle, false, null, t.RegionSizeX, t.RegionSizeY) == null)
|
if (Connect(ip, port, handle, false, null, t.RegionSizeX, t.RegionSizeY) == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ namespace OpenMetaverse
|
||||||
|
|
||||||
/// <summary>Enable/disable storing terrain heightmaps in the
|
/// <summary>Enable/disable storing terrain heightmaps in the
|
||||||
/// TerrainManager</summary>
|
/// TerrainManager</summary>
|
||||||
public bool STORE_LAND_PATCHES = false;
|
public bool STORE_LAND_PATCHES = true;
|
||||||
|
|
||||||
/// <summary>Enable/disable sending agent updates</summary>
|
/// <summary>Enable/disable sending agent updates</summary>
|
||||||
public bool SEND_AGENT_UPDATES = true;
|
public bool SEND_AGENT_UPDATES = true;
|
||||||
|
|
@ -208,7 +208,7 @@ namespace OpenMetaverse
|
||||||
/// <summary>Should we connect to multiple sims? This will allow
|
/// <summary>Should we connect to multiple sims? This will allow
|
||||||
/// viewing into neighboring simulators and sim crossings
|
/// viewing into neighboring simulators and sim crossings
|
||||||
/// (Experimental)</summary>
|
/// (Experimental)</summary>
|
||||||
public bool MULTIPLE_SIMS = false;
|
public bool MULTIPLE_SIMS = true;
|
||||||
|
|
||||||
/// <summary>If true, all object update packets will be decoded in to
|
/// <summary>If true, all object update packets will be decoded in to
|
||||||
/// native objects. If false, only updates for our own agent will be
|
/// native objects. If false, only updates for our own agent will be
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,27 @@ namespace SecondBotEvents.Commands
|
||||||
List<NearMeDetails> BetterNearMe = [];
|
List<NearMeDetails> BetterNearMe = [];
|
||||||
|
|
||||||
Utils.LongToUInts(GetClient().Network.CurrentSim.Handle, out uint mySimX, out uint mySimY);
|
Utils.LongToUInts(GetClient().Network.CurrentSim.Handle, out uint mySimX, out uint mySimY);
|
||||||
Vector3 myGlobalPos = new Vector3(mySimX + GetClient().Self.SimPosition.X, mySimY + GetClient().Self.SimPosition.Y, GetClient().Self.SimPosition.Z);
|
Vector3 myLocalPos = GetClient().Self.SimPosition;
|
||||||
|
|
||||||
foreach (var sim in GetClient().Network.Simulators)
|
List<Simulator> sims = [];
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
lock (GetClient().Network.Simulators)
|
||||||
|
{
|
||||||
|
sims = [.. GetClient().Network.Simulators];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
sims = [GetClient().Network.CurrentSim];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sim in sims)
|
||||||
|
{
|
||||||
|
if (sim == null || !sim.Connected) continue;
|
||||||
Utils.LongToUInts(sim.Handle, out uint simX, out uint simY);
|
Utils.LongToUInts(sim.Handle, out uint simX, out uint simY);
|
||||||
|
float offsetX = (long)simX - (long)mySimX;
|
||||||
|
float offsetY = (long)simY - (long)mySimY;
|
||||||
HashSet<UUID> processedAvatars = [];
|
HashSet<UUID> processedAvatars = [];
|
||||||
|
|
||||||
List<Avatar> avcopy = [.. sim.ObjectsAvatars.Values];
|
List<Avatar> avcopy = [.. sim.ObjectsAvatars.Values];
|
||||||
|
|
@ -100,21 +116,20 @@ namespace SecondBotEvents.Commands
|
||||||
localPos = parentObj.Position + (av.Position * parentObj.Rotation);
|
localPos = parentObj.Position + (av.Position * parentObj.Rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 globalPos = new Vector3(simX + localPos.X, simY + localPos.Y, localPos.Z);
|
float relX = offsetX + localPos.X;
|
||||||
double dist = Math.Round(Vector3.Distance(globalPos, myGlobalPos), 2);
|
float relY = offsetY + localPos.Y;
|
||||||
|
Vector3 relPos = new Vector3(relX, relY, localPos.Z);
|
||||||
|
double dist = Math.Round(Vector3.Distance(relPos, myLocalPos), 2);
|
||||||
|
|
||||||
if (dist > 150) continue;
|
if (dist > 150) continue;
|
||||||
|
|
||||||
float relX = (simX - mySimX) + localPos.X;
|
|
||||||
float relY = (simY - mySimY) + localPos.Y;
|
|
||||||
|
|
||||||
NearMeDetails details = new()
|
NearMeDetails details = new()
|
||||||
{
|
{
|
||||||
id = av.ID.ToString(),
|
id = av.ID.ToString(),
|
||||||
name = av.Name,
|
name = av.Name,
|
||||||
x = (int)Math.Round(relX),
|
x = (int)Math.Round(relX),
|
||||||
y = (int)Math.Round(relY),
|
y = (int)Math.Round(relY),
|
||||||
z = (int)Math.Round(globalPos.Z),
|
z = (int)Math.Round(localPos.Z),
|
||||||
range = dist
|
range = dist
|
||||||
};
|
};
|
||||||
BetterNearMe.Add(details);
|
BetterNearMe.Add(details);
|
||||||
|
|
@ -126,14 +141,13 @@ namespace SecondBotEvents.Commands
|
||||||
if (avID == GetClient().Self.AgentID || processedAvatars.Contains(avID)) continue;
|
if (avID == GetClient().Self.AgentID || processedAvatars.Contains(avID)) continue;
|
||||||
|
|
||||||
Vector3 localPos = kvp.Value;
|
Vector3 localPos = kvp.Value;
|
||||||
Vector3 globalPos = new Vector3(simX + localPos.X, simY + localPos.Y, localPos.Z);
|
float relX = offsetX + localPos.X;
|
||||||
double dist = Math.Round(Vector3.Distance(globalPos, myGlobalPos), 2);
|
float relY = offsetY + localPos.Y;
|
||||||
|
Vector3 relPos = new Vector3(relX, relY, localPos.Z);
|
||||||
|
double dist = Math.Round(Vector3.Distance(relPos, myLocalPos), 2);
|
||||||
|
|
||||||
if (dist > 150) continue;
|
if (dist > 150) continue;
|
||||||
|
|
||||||
float relX = (simX - mySimX) + localPos.X;
|
|
||||||
float relY = (simY - mySimY) + localPos.Y;
|
|
||||||
|
|
||||||
string avName = master.DataStoreService.GetAvatarName(avID);
|
string avName = master.DataStoreService.GetAvatarName(avID);
|
||||||
if (avName == "lookup" || string.IsNullOrEmpty(avName)) avName = "Unknown";
|
if (avName == "lookup" || string.IsNullOrEmpty(avName)) avName = "Unknown";
|
||||||
|
|
||||||
|
|
@ -143,7 +157,7 @@ namespace SecondBotEvents.Commands
|
||||||
name = avName,
|
name = avName,
|
||||||
x = (int)Math.Round(relX),
|
x = (int)Math.Round(relX),
|
||||||
y = (int)Math.Round(relY),
|
y = (int)Math.Round(relY),
|
||||||
z = (int)Math.Round(globalPos.Z),
|
z = (int)Math.Round(localPos.Z),
|
||||||
range = dist
|
range = dist
|
||||||
};
|
};
|
||||||
BetterNearMe.Add(details);
|
BetterNearMe.Add(details);
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,17 @@ namespace SecondBotEvents.Commands
|
||||||
}
|
}
|
||||||
master.DataStoreService.OpenChatWindow(false, avataruuid, avataruuid);
|
master.DataStoreService.OpenChatWindow(false, avataruuid, avataruuid);
|
||||||
master.BotClient.SendIM(avataruuid, message);
|
master.BotClient.SendIM(avataruuid, message);
|
||||||
|
string senderName = "{BOT} " + GetClient().Self.Name;
|
||||||
|
master.DataStoreService.RecordAvatarIm(avataruuid, senderName, message);
|
||||||
|
if (master.DiscordService != null && master.DiscordService.isRunning())
|
||||||
|
{
|
||||||
|
string avName = master.DataStoreService.GetAvatarName(avataruuid);
|
||||||
|
if (string.IsNullOrEmpty(avName) || avName == "lookup")
|
||||||
|
{
|
||||||
|
avName = avatar;
|
||||||
|
}
|
||||||
|
master.DiscordService.AvatarIMChat(avataruuid, avName, message, GetClient().Self.Name);
|
||||||
|
}
|
||||||
return BasicReply("ok", [avatar, message]);
|
return BasicReply("ok", [avatar, message]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,8 @@ namespace SecondBotEvents.Commands
|
||||||
return Failure("Unknown group", [group]);
|
return Failure("Unknown group", [group]);
|
||||||
}
|
}
|
||||||
GetClient().Self.InstantMessageGroup(groupUUID, message);
|
GetClient().Self.InstantMessageGroup(groupUUID, message);
|
||||||
|
string senderName = "{BOT} " + GetClient().Self.Name;
|
||||||
|
master.DataStoreService.RecordGroupChat(groupUUID, senderName, message);
|
||||||
return BasicReply("Sending");
|
return BasicReply("Sending");
|
||||||
}
|
}
|
||||||
[About("fetchs a list of all pending group invitations")]
|
[About("fetchs a list of all pending group invitations")]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1301,6 +1301,184 @@ namespace SecondBotEvents.Commands
|
||||||
}
|
}
|
||||||
return UUID.Zero;
|
return UUID.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Gets the list of pending approval inventory items")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetPendingInventory()
|
||||||
|
{
|
||||||
|
var interaction = master.InteractionService;
|
||||||
|
if (interaction == null)
|
||||||
|
{
|
||||||
|
return Failure("Interaction service not found");
|
||||||
|
}
|
||||||
|
UUID pendingFolderUUID = interaction.GetOrCreatePendingFolder();
|
||||||
|
|
||||||
|
List<InventoryBase> contents = GetClient().Inventory.FolderContents(
|
||||||
|
pendingFolderUUID,
|
||||||
|
GetClient().Self.AgentID,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
InventorySortOrder.ByName,
|
||||||
|
TimeSpan.FromSeconds(15)
|
||||||
|
);
|
||||||
|
|
||||||
|
var items = new List<Dictionary<string, string>>();
|
||||||
|
if (contents != null)
|
||||||
|
{
|
||||||
|
foreach (var item in contents)
|
||||||
|
{
|
||||||
|
if (item is InventoryItem itm)
|
||||||
|
{
|
||||||
|
var entry = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "uuid", itm.UUID.ToString() },
|
||||||
|
{ "name", itm.Name },
|
||||||
|
{ "type", itm.AssetType.ToString() },
|
||||||
|
{ "description", itm.Description }
|
||||||
|
};
|
||||||
|
items.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BasicReply(JsonSerializer.Serialize(items, JsonOptions.UnsafeRelaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Resolves a pending inventory item by either accepting it or declining it")]
|
||||||
|
[ArgHints("itemUuid", "The UUID of the item to resolve", "UUID")]
|
||||||
|
[ArgHints("action", "Accept or Decline", "Text")]
|
||||||
|
[CmdTypeDo()]
|
||||||
|
public object ResolvePendingItem(string itemUuid, string action)
|
||||||
|
{
|
||||||
|
if (!UUID.TryParse(itemUuid, out UUID targetItemUUID))
|
||||||
|
{
|
||||||
|
return Failure("Invalid item UUID");
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = GetClient().Inventory.GetStore;
|
||||||
|
if (!store.Items.ContainsKey(targetItemUUID))
|
||||||
|
{
|
||||||
|
return Failure("Item not found in inventory cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryItem item = store.Items[targetItemUUID].Data as InventoryItem;
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return Failure("Target is not an inventory item");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.Equals("Accept", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
UUID destFolderUUID = GetClient().Inventory.FindFolderForType(item.AssetType);
|
||||||
|
GetClient().Inventory.MoveItem(targetItemUUID, destFolderUUID);
|
||||||
|
return BasicReply("Accepted and moved item to default folder");
|
||||||
|
}
|
||||||
|
else if (action.Equals("Decline", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
GetClient().Inventory.RemoveItem(targetItemUUID);
|
||||||
|
return BasicReply("Declined and deleted item");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Failure("Invalid action. Must be 'Accept' or 'Decline'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Touches/clicks a worn attachment by its inventory item UUID")]
|
||||||
|
[ArgHints("itemUuid", "The inventory UUID of the worn attachment to touch", "UUID")]
|
||||||
|
[CmdTypeDo()]
|
||||||
|
public object TouchAttachment(string itemUuid)
|
||||||
|
{
|
||||||
|
if (!UUID.TryParse(itemUuid, out UUID targetUUID))
|
||||||
|
{
|
||||||
|
return Failure("Invalid inventory item UUID");
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
UUID botID = client.Self.AgentID;
|
||||||
|
|
||||||
|
// Find only the BOT's own avatar
|
||||||
|
Avatar botAvatar = null;
|
||||||
|
foreach (var av in client.Network.CurrentSim.ObjectsAvatars.Values)
|
||||||
|
{
|
||||||
|
if (av.ID == botID) { botAvatar = av; break; }
|
||||||
|
}
|
||||||
|
if (botAvatar == null)
|
||||||
|
{
|
||||||
|
return Failure("Bot avatar not found in sim");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attachment in botAvatar.GetChildren(client))
|
||||||
|
{
|
||||||
|
bool matched = false;
|
||||||
|
|
||||||
|
// Primary: check NameValues for AttachItemID (set by SL server for attached objects)
|
||||||
|
if (attachment.NameValues != null)
|
||||||
|
{
|
||||||
|
foreach (var nv in attachment.NameValues)
|
||||||
|
{
|
||||||
|
if (nv.Name == "AttachItemID" && nv.Value != null)
|
||||||
|
{
|
||||||
|
if (UUID.TryParse(nv.Value.ToString(), out UUID attachInvUUID) && attachInvUUID == targetUUID)
|
||||||
|
{
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: direct object UUID match (rare but possible)
|
||||||
|
if (!matched && attachment.ID == targetUUID) matched = true;
|
||||||
|
|
||||||
|
if (matched)
|
||||||
|
{
|
||||||
|
client.Objects.ClickObject(client.Network.CurrentSim, attachment.LocalID);
|
||||||
|
return BasicReply("Touched attachment localID=" + attachment.LocalID + " objectUUID=" + attachment.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Failure("Attachment not found - use GetAttachmentDebug to inspect available attachments");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Returns debug info for all attachments on the bot avatar - use this to diagnose TouchAttachment issues")]
|
||||||
|
[CmdTypeGet()]
|
||||||
|
public object GetAttachmentDebug()
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
UUID botID = client.Self.AgentID;
|
||||||
|
|
||||||
|
Avatar botAvatar = null;
|
||||||
|
foreach (var av in client.Network.CurrentSim.ObjectsAvatars.Values)
|
||||||
|
{
|
||||||
|
if (av.ID == botID) { botAvatar = av; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (botAvatar == null) return Failure("Bot avatar not found in sim");
|
||||||
|
|
||||||
|
var results = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var attachment in botAvatar.GetChildren(client))
|
||||||
|
{
|
||||||
|
var entry = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "objectUUID", attachment.ID.ToString() },
|
||||||
|
{ "localID", attachment.LocalID.ToString() },
|
||||||
|
{ "parentID", attachment.ParentID.ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attachment.Properties != null)
|
||||||
|
entry["name"] = attachment.Properties.Name ?? "";
|
||||||
|
|
||||||
|
if (attachment.NameValues != null)
|
||||||
|
{
|
||||||
|
foreach (var nv in attachment.NameValues)
|
||||||
|
entry["nv_" + nv.Name] = nv.Value?.ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicReply(JsonSerializer.Serialize(results, JsonOptions.UnsafeRelaxed));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InventoryNodeEntry
|
public class InventoryNodeEntry
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.Json;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using OpenMetaverse.ImportExport.Collada14;
|
using OpenMetaverse.ImportExport.Collada14;
|
||||||
using SecondBotEvents.Services;
|
using SecondBotEvents.Services;
|
||||||
|
|
@ -273,6 +274,41 @@ namespace SecondBotEvents.Commands
|
||||||
return BasicReply("Removed perm: " + flag + " Sticky: " + stickyflag.ToString(), [avatar, flag, state, sticky]);
|
return BasicReply("Removed perm: " + flag + " Sticky: " + stickyflag.ToString(), [avatar, flag, state, sticky]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[About("Clears all active RLV restrictions on the bot")]
|
||||||
|
[ReturnHints("ok")]
|
||||||
|
[CmdTypeDo()]
|
||||||
|
public object RLVClearAll()
|
||||||
|
{
|
||||||
|
if (master.RLV == null)
|
||||||
|
{
|
||||||
|
return Failure("RLV service is not running");
|
||||||
|
}
|
||||||
|
lock (master.RLV.Rules)
|
||||||
|
{
|
||||||
|
master.RLV.Rules.Clear();
|
||||||
|
}
|
||||||
|
return BasicReply("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
[About("Clears a specific active RLV restriction behavior on the bot")]
|
||||||
|
[ArgHints("behaviour", "The restriction behavior to clear", "Text", "unsit")]
|
||||||
|
[ReturnHints("ok")]
|
||||||
|
[CmdTypeDo()]
|
||||||
|
public object RLVClearBehaviour(string behaviour)
|
||||||
|
{
|
||||||
|
if (master.RLV == null)
|
||||||
|
{
|
||||||
|
return Failure("RLV service is not running");
|
||||||
|
}
|
||||||
|
lock (master.RLV.Rules)
|
||||||
|
{
|
||||||
|
master.RLV.Rules.RemoveAll(r => r.Behaviour.Equals(behaviour, StringComparison.CurrentCultureIgnoreCase) ||
|
||||||
|
(behaviour.Equals("stand", StringComparison.CurrentCultureIgnoreCase) && r.Behaviour.Equals("unsit", StringComparison.CurrentCultureIgnoreCase)) ||
|
||||||
|
(behaviour.Equals("unsit", StringComparison.CurrentCultureIgnoreCase) && r.Behaviour.Equals("stand", StringComparison.CurrentCultureIgnoreCase)));
|
||||||
|
}
|
||||||
|
return BasicReply("ok", [behaviour]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,12 @@ private BotRejoinConfig rejoinCfg;
|
||||||
|
|
||||||
protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e)
|
protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e)
|
||||||
{
|
{
|
||||||
goodticks = 0;
|
|
||||||
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
|
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
|
||||||
|
if (client?.Network?.CurrentSim != null && e.Simulator != client.Network.CurrentSim && client.Network.CurrentSim.Connected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goodticks = 0;
|
||||||
if (!getLogoutExpected())
|
if (!getLogoutExpected())
|
||||||
{
|
{
|
||||||
HandleUnexpectedDisconnect("Unexpected sim disconnect");
|
HandleUnexpectedDisconnect("Unexpected sim disconnect");
|
||||||
|
|
@ -290,6 +294,8 @@ private BotRejoinConfig rejoinCfg;
|
||||||
{
|
{
|
||||||
client = null;
|
client = null;
|
||||||
client = new GridClient();
|
client = new GridClient();
|
||||||
|
client.Settings.STORE_LAND_PATCHES = true;
|
||||||
|
client.Settings.MULTIPLE_SIMS = true;
|
||||||
client.Network.SimConnected += BotSimConnected;
|
client.Network.SimConnected += BotSimConnected;
|
||||||
client.Network.LoggedOut += BotLoggedOut;
|
client.Network.LoggedOut += BotLoggedOut;
|
||||||
client.Network.Disconnected += BotDisconnected;
|
client.Network.Disconnected += BotDisconnected;
|
||||||
|
|
|
||||||
|
|
@ -145,13 +145,13 @@ namespace SecondBotEvents.Services
|
||||||
if(myConfig.GetObjectMasterOptout() == true)
|
if(myConfig.GetObjectMasterOptout() == true)
|
||||||
{
|
{
|
||||||
string ownername = master.DataStoreService.GetAvatarName(e.IM.FromAgentID);
|
string ownername = master.DataStoreService.GetAvatarName(e.IM.FromAgentID);
|
||||||
requireSigning = !myConfig.GetMastersCSV().Contains(ownername);
|
requireSigning = !IsMaster(ownername);
|
||||||
if ((requireSigning == false) && (myConfig.GetCheckDotNames() == true))
|
if (requireSigning && myConfig.GetCheckDotNames())
|
||||||
{
|
{
|
||||||
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
|
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
|
||||||
if (bits.Length == 2)
|
if (bits.Length == 2)
|
||||||
{
|
{
|
||||||
requireSigning = myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
|
requireSigning = !IsMaster(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,13 +170,13 @@ namespace SecondBotEvents.Services
|
||||||
}
|
}
|
||||||
master.DataStoreService.AddAvatar(e.IM.FromAgentID, e.IM.FromAgentName);
|
master.DataStoreService.AddAvatar(e.IM.FromAgentID, e.IM.FromAgentName);
|
||||||
requireSigning = false;
|
requireSigning = false;
|
||||||
acceptMessage = myConfig.GetMastersCSV().Contains(e.IM.FromAgentName);
|
acceptMessage = IsMaster(e.IM.FromAgentName);
|
||||||
if ((acceptMessage == false) && (myConfig.GetCheckDotNames() == true))
|
if ((acceptMessage == false) && (myConfig.GetCheckDotNames() == true))
|
||||||
{
|
{
|
||||||
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
|
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
|
||||||
if (bits.Length == 2)
|
if (bits.Length == 2)
|
||||||
{
|
{
|
||||||
acceptMessage = myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper()+" " + bits[1].FirstCharToUpper());
|
acceptMessage = IsMaster(bits[0].FirstCharToUpper()+" " + bits[1].FirstCharToUpper());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -193,20 +193,26 @@ namespace SecondBotEvents.Services
|
||||||
CommandInterfaceCaller(e.IM.Message, requireSigning);
|
CommandInterfaceCaller(e.IM.Message, requireSigning);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AvatarUUIDIsMaster(UUID avatar)
|
public bool IsMaster(string name)
|
||||||
{
|
{
|
||||||
foreach(string name in myConfig.GetMastersCSV())
|
if (string.IsNullOrEmpty(name)) return false;
|
||||||
|
foreach (string masterName in myConfig.GetMastersCSV())
|
||||||
{
|
{
|
||||||
string find = master.DataStoreService.GetAvatarUUID(name);
|
if (SecondbotHelpers.IsNameMatch(masterName, name))
|
||||||
if (find != "lookup")
|
|
||||||
{
|
{
|
||||||
if(avatar.ToString() == find)
|
return true;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AvatarUUIDIsMaster(UUID avatar)
|
||||||
|
{
|
||||||
|
string name = master.DataStoreService.GetAvatarName(avatar);
|
||||||
|
if (name != "lookup")
|
||||||
|
{
|
||||||
|
return IsMaster(name);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,10 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (chatWindowsOwner.ContainsValue(entry.Value) == true)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
clearLinks.Add(entry.Value, entry.Key);
|
clearLinks.Add(entry.Value, entry.Key);
|
||||||
}
|
}
|
||||||
lock (avatarLastUsed) lock (avatarsName2Key) lock(avatarsKey2Displayname)
|
lock (avatarLastUsed) lock (avatarsName2Key) lock(avatarsKey2Displayname)
|
||||||
|
|
@ -406,25 +410,167 @@ namespace SecondBotEvents.Services
|
||||||
|
|
||||||
public string GetGroupMemberRank(UUID group, UUID avatar)
|
public string GetGroupMemberRank(UUID group, UUID avatar)
|
||||||
{
|
{
|
||||||
|
bool needFetch = false;
|
||||||
lock (groupMembers)
|
lock (groupMembers)
|
||||||
{
|
{
|
||||||
if (groupMembers.ContainsKey(group) == false || groupMembers[group].ContainsKey(avatar) == false)
|
if (groupMembers.ContainsKey(group) == false || groupMembers[group].ContainsKey(avatar) == false)
|
||||||
{
|
{
|
||||||
LogFormater.Info($"[DEBUG] GetGroupMemberRank: Returning Guest. GroupKnown: {groupMembers.ContainsKey(group)}, AvatarKnown: {(groupMembers.ContainsKey(group) ? groupMembers[group].ContainsKey(avatar).ToString() : "N/A")} for Group: {group}, Avatar: {avatar}");
|
needFetch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needFetch)
|
||||||
|
{
|
||||||
|
AutoResetEvent fetchEvent = new(false);
|
||||||
|
void callback(object sender, GroupMembersReplyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GroupID == group)
|
||||||
|
{
|
||||||
|
fetchEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GetClient().Groups.GroupMembersReply += callback;
|
||||||
|
GetClient().Groups.RequestGroupMembers(group);
|
||||||
|
fetchEvent.WaitOne(3000, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogFormater.Warn($"GetGroupMemberRank exception during RequestGroupMembers: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GetClient().Groups.GroupMembersReply -= callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (groupMembers)
|
||||||
|
{
|
||||||
|
if (groupMembers.ContainsKey(group) == false || groupMembers[group].ContainsKey(avatar) == false)
|
||||||
|
{
|
||||||
|
LogFormater.Info($"[DEBUG] GetGroupMemberRank: Returning Guest after fetch attempt. GroupKnown: {groupMembers.ContainsKey(group)}, AvatarKnown: {(groupMembers.ContainsKey(group) ? groupMembers[group].ContainsKey(avatar).ToString() : "N/A")} for Group: {group}, Avatar: {avatar}");
|
||||||
return "Guest";
|
return "Guest";
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupMember m = groupMembers[group][avatar];
|
GroupMember m = groupMembers[group][avatar];
|
||||||
if (m.IsOwner == true) return "Owner";
|
if (m.IsOwner == true) return "Owner";
|
||||||
if (m.Title != null && (m.Title.Contains("Officer") || m.Title.Contains("Officer"))) return "Officer";
|
|
||||||
|
// Check for Owner powers (DeleteRole, ChangeActions, ChangeIdentity)
|
||||||
// Also check roles if title didn't match
|
GroupPowers ownerPowers = GroupPowers.DeleteRole | GroupPowers.ChangeActions | GroupPowers.ChangeIdentity;
|
||||||
if (groupRoles.ContainsKey(group))
|
if ((m.Powers & ownerPowers) == ownerPowers) return "Owner";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(m.Title) == false)
|
||||||
{
|
{
|
||||||
// This is a bit complex as we need member-to-role mapping which we don't have cached yet
|
if (m.Title.Contains("Owner", StringComparison.OrdinalIgnoreCase) || m.Title.Contains("Founder", StringComparison.OrdinalIgnoreCase))
|
||||||
// For now, Owner and Title check is a good start.
|
{
|
||||||
|
return "Owner";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "Member";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch group roles and role-member mapping to check assigned group roles
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Fetch group roles
|
||||||
|
Dictionary<UUID, GroupRole> rolesDict = new();
|
||||||
|
AutoResetEvent rolesFetchEvent = new(false);
|
||||||
|
void rolesCallback(object sender, GroupRolesDataReplyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GroupID == group)
|
||||||
|
{
|
||||||
|
rolesDict = e.Roles;
|
||||||
|
rolesFetchEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GetClient().Groups.GroupRoleDataReply += rolesCallback;
|
||||||
|
GetClient().Groups.RequestGroupRoles(group);
|
||||||
|
rolesFetchEvent.WaitOne(2000, false);
|
||||||
|
GetClient().Groups.GroupRoleDataReply -= rolesCallback;
|
||||||
|
|
||||||
|
// Fetch group role-members map
|
||||||
|
List<KeyValuePair<UUID, UUID>> roleMembersList = new();
|
||||||
|
AutoResetEvent membersFetchEvent = new(false);
|
||||||
|
void roleMembersCallback(object sender, GroupRolesMembersReplyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GroupID == group)
|
||||||
|
{
|
||||||
|
roleMembersList = e.RolesMembers;
|
||||||
|
membersFetchEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GetClient().Groups.GroupRoleMembersReply += roleMembersCallback;
|
||||||
|
GetClient().Groups.RequestGroupRolesMembers(group);
|
||||||
|
membersFetchEvent.WaitOne(2000, false);
|
||||||
|
GetClient().Groups.GroupRoleMembersReply -= roleMembersCallback;
|
||||||
|
|
||||||
|
// Check if avatar belongs to any Owner or Officer role
|
||||||
|
HashSet<UUID> ownerRoleIDs = new();
|
||||||
|
HashSet<UUID> officerRoleIDs = new();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<UUID, GroupRole> r in rolesDict)
|
||||||
|
{
|
||||||
|
string rName = r.Value.Name ?? "";
|
||||||
|
string rTitle = r.Value.Title ?? "";
|
||||||
|
GroupPowers rPowers = r.Value.Powers;
|
||||||
|
|
||||||
|
GroupPowers ownerPowers = GroupPowers.DeleteRole | GroupPowers.ChangeActions | GroupPowers.ChangeIdentity;
|
||||||
|
if (rName.Equals("Owners", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
rName.Equals("Owner", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
rTitle.Contains("Owner", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(rPowers & ownerPowers) == ownerPowers)
|
||||||
|
{
|
||||||
|
ownerRoleIDs.Add(r.Value.ID);
|
||||||
|
}
|
||||||
|
else if (rName.Contains("Officer", StringComparison.OrdinalIgnoreCase) || rTitle.Contains("Officer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
officerRoleIDs.Add(r.Value.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOwnerRole = false;
|
||||||
|
bool isOfficerRole = false;
|
||||||
|
|
||||||
|
foreach (KeyValuePair<UUID, UUID> pair in roleMembersList)
|
||||||
|
{
|
||||||
|
if (pair.Value == avatar) // pair.Value = MemberID, pair.Key = RoleID
|
||||||
|
{
|
||||||
|
if (ownerRoleIDs.Contains(pair.Key))
|
||||||
|
{
|
||||||
|
isOwnerRole = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (officerRoleIDs.Contains(pair.Key))
|
||||||
|
{
|
||||||
|
isOfficerRole = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOwnerRole) return "Owner";
|
||||||
|
if (isOfficerRole) return "Officer";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogFormater.Warn($"GetGroupMemberRank role check exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (groupMembers)
|
||||||
|
{
|
||||||
|
if (groupMembers.ContainsKey(group) && groupMembers[group].ContainsKey(avatar))
|
||||||
|
{
|
||||||
|
GroupMember m = groupMembers[group][avatar];
|
||||||
|
if (string.IsNullOrEmpty(m.Title) == false && m.Title.Contains("Officer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "Officer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Member";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RequestLoginToken(UUID avatar)
|
public string RequestLoginToken(UUID avatar)
|
||||||
|
|
@ -582,6 +728,40 @@ namespace SecondBotEvents.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RecordAvatarIm(UUID avatarId, string fromName, string message)
|
||||||
|
{
|
||||||
|
lock (chatWindows)
|
||||||
|
{
|
||||||
|
UUID sessionID = UUID.Zero;
|
||||||
|
foreach (KeyValuePair<UUID, UUID> kvp in chatWindowsOwner)
|
||||||
|
{
|
||||||
|
if (kvp.Value == avatarId)
|
||||||
|
{
|
||||||
|
sessionID = kvp.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sessionID == UUID.Zero)
|
||||||
|
{
|
||||||
|
sessionID = avatarId;
|
||||||
|
OpenChatWindow(false, sessionID, avatarId);
|
||||||
|
}
|
||||||
|
RecordChat(sessionID, fromName, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordGroupChat(UUID groupId, string fromName, string message)
|
||||||
|
{
|
||||||
|
lock (chatWindows)
|
||||||
|
{
|
||||||
|
if (!chatWindows.ContainsKey(groupId))
|
||||||
|
{
|
||||||
|
OpenChatWindow(true, groupId, groupId);
|
||||||
|
}
|
||||||
|
RecordChat(groupId, fromName, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void CloseChat(UUID owner)
|
public void CloseChat(UUID owner)
|
||||||
{
|
{
|
||||||
lock (chatWindows)
|
lock (chatWindows)
|
||||||
|
|
@ -862,6 +1042,7 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
if (av.Key == avatarId)
|
if (av.Key == avatarId)
|
||||||
{
|
{
|
||||||
|
AddAvatar(av.Key, av.Value);
|
||||||
hasReply = true;
|
hasReply = true;
|
||||||
fetchEvent.Set();
|
fetchEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
@ -918,15 +1099,11 @@ namespace SecondBotEvents.Services
|
||||||
public bool KnownAvatar(string name, UUID avatarId)
|
public bool KnownAvatar(string name, UUID avatarId)
|
||||||
{
|
{
|
||||||
SyncAvatarsHashMap();
|
SyncAvatarsHashMap();
|
||||||
if (avatarId == UUID.Zero)
|
if (avatarId != UUID.Zero && KnownAvatarUUID(avatarId) == true)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if(KnownAvatarUUID(avatarId) == true)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(KnownAvatarName(name) == true)
|
if (!string.IsNullOrEmpty(name) && KnownAvatarName(name) == true)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1829,21 +2006,93 @@ namespace SecondBotEvents.Services
|
||||||
displayNameFinder(id);
|
displayNameFinder(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
avatarsName2Key.Add(name, id);
|
avatarsName2Key[name] = id;
|
||||||
avatarLastUsed.Add(id, SecondbotHelpers.UnixTimeNow());
|
avatarLastUsed[id] = SecondbotHelpers.UnixTimeNow();
|
||||||
avatarsKey2Displayname.Add(id, "?");
|
if (avatarsKey2Displayname.ContainsKey(id) == false)
|
||||||
|
{
|
||||||
|
avatarsKey2Displayname.Add(id, "?");
|
||||||
|
}
|
||||||
displayNameFinder(id);
|
displayNameFinder(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetDisplayName(UUID id)
|
public string GetDisplayName(UUID id)
|
||||||
{
|
{
|
||||||
requestDisplayName(id);
|
if (avatarsKey2Displayname.ContainsKey(id) && avatarsKey2Displayname[id] != "?" && !string.IsNullOrEmpty(avatarsKey2Displayname[id]))
|
||||||
if (avatarsKey2Displayname.ContainsKey(id) == false)
|
|
||||||
{
|
{
|
||||||
return "?";
|
return avatarsKey2Displayname[id];
|
||||||
}
|
}
|
||||||
return avatarsKey2Displayname[id];
|
lock (avatarsKey2Displayname)
|
||||||
|
{
|
||||||
|
if (!avatarsKey2Displayname.ContainsKey(id))
|
||||||
|
{
|
||||||
|
avatarsKey2Displayname[id] = "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AutoResetEvent waitEvent = new(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = GetClient().Avatars.GetDisplayNames(new List<UUID> { id }, (success, agents, badIds) =>
|
||||||
|
{
|
||||||
|
if (success && agents != null && agents.Length > 0)
|
||||||
|
{
|
||||||
|
foreach (var ag in agents)
|
||||||
|
{
|
||||||
|
if (ag.ID == id)
|
||||||
|
{
|
||||||
|
lock (avatarsKey2Displayname) lock (avatarsName2Key)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(ag.DisplayName))
|
||||||
|
{
|
||||||
|
avatarsKey2Displayname[id] = ag.DisplayName;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(ag.LegacyFullName))
|
||||||
|
{
|
||||||
|
avatarsName2Key[ag.LegacyFullName] = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitEvent.Set();
|
||||||
|
});
|
||||||
|
waitEvent.WaitOne(3000);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
// Fallback to RequestAgentProfile if GetDisplayNames didn't resolve
|
||||||
|
if (!avatarsKey2Displayname.ContainsKey(id) || avatarsKey2Displayname[id] == "?")
|
||||||
|
{
|
||||||
|
AutoResetEvent profileWait = new(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = GetClient().Avatars.RequestAgentProfile(id, (status, properties) =>
|
||||||
|
{
|
||||||
|
if (properties != null && !string.IsNullOrEmpty(properties.DisplayName))
|
||||||
|
{
|
||||||
|
lock (avatarsKey2Displayname)
|
||||||
|
{
|
||||||
|
avatarsKey2Displayname[id] = properties.DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileWait.Set();
|
||||||
|
});
|
||||||
|
profileWait.WaitOne(2000);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarsKey2Displayname.ContainsKey(id) && avatarsKey2Displayname[id] != "?" && !string.IsNullOrEmpty(avatarsKey2Displayname[id]))
|
||||||
|
{
|
||||||
|
return avatarsKey2Displayname[id];
|
||||||
|
}
|
||||||
|
string avName = GetAvatarName(id);
|
||||||
|
if (!string.IsNullOrEmpty(avName) && avName != "lookup")
|
||||||
|
{
|
||||||
|
return avName;
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AvatarUUIDName(object o, UUIDNameReplyEventArgs e)
|
protected void AvatarUUIDName(object o, UUIDNameReplyEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,18 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplication check: ignore duplicate dialog from same object with same message if already waiting
|
||||||
|
CleanUp();
|
||||||
|
foreach (var existing in DialogWindows.Values)
|
||||||
|
{
|
||||||
|
if (existing.ObjectID == e.ObjectID && existing.Message == e.Message && existing.Channel == e.Channel)
|
||||||
|
{
|
||||||
|
LogFormater.Info($"[DialogService] Ignoring duplicate dialog from {e.ObjectName}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int windowid = nextDialogWindow;
|
int windowid = nextDialogWindow;
|
||||||
nextDialogWindow++;
|
nextDialogWindow++;
|
||||||
if(nextDialogWindow > 999)
|
if(nextDialogWindow > 999)
|
||||||
|
|
@ -251,13 +263,14 @@ namespace SecondBotEvents.Services
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LogFormater.Info("Dialog service [Attached to new client]");
|
LogFormater.Info("Dialog service [Attached to new client]");
|
||||||
|
GetClient().Network.LoggedOut -= BotLoggedOut;
|
||||||
GetClient().Network.LoggedOut += BotLoggedOut;
|
GetClient().Network.LoggedOut += BotLoggedOut;
|
||||||
|
GetClient().Network.SimConnected -= BotLoggedIn;
|
||||||
GetClient().Network.SimConnected += BotLoggedIn;
|
GetClient().Network.SimConnected += BotLoggedIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
|
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
|
||||||
{
|
{
|
||||||
GetClient().Network.SimConnected += BotLoggedIn;
|
|
||||||
LogFormater.Info("Dialog service [waiting for new client]");
|
LogFormater.Info("Dialog service [waiting for new client]");
|
||||||
if (GetClient() != null)
|
if (GetClient() != null)
|
||||||
{
|
{
|
||||||
|
|
@ -271,18 +284,24 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
GetClient().Network.SimConnected -= BotLoggedIn;
|
GetClient().Network.SimConnected -= BotLoggedIn;
|
||||||
botConnected = true;
|
botConnected = true;
|
||||||
// attach events
|
// attach events cleanly
|
||||||
|
GetClient().Self.ScriptDialog -= DialogWindowEvent;
|
||||||
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
||||||
|
GetClient().Self.LoadURL -= LoadUrlEvent;
|
||||||
GetClient().Self.LoadURL += LoadUrlEvent;
|
GetClient().Self.LoadURL += LoadUrlEvent;
|
||||||
|
GetClient().Self.AlertMessage -= BotAlertMessage;
|
||||||
GetClient().Self.AlertMessage += BotAlertMessage;
|
GetClient().Self.AlertMessage += BotAlertMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Attach()
|
public void Attach()
|
||||||
{
|
{
|
||||||
botConnected = true;
|
botConnected = true;
|
||||||
// attach events
|
// attach events cleanly
|
||||||
|
GetClient().Self.ScriptDialog -= DialogWindowEvent;
|
||||||
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
GetClient().Self.ScriptDialog += DialogWindowEvent;
|
||||||
|
GetClient().Self.LoadURL -= LoadUrlEvent;
|
||||||
GetClient().Self.LoadURL += LoadUrlEvent;
|
GetClient().Self.LoadURL += LoadUrlEvent;
|
||||||
|
GetClient().Self.AlertMessage -= BotAlertMessage;
|
||||||
GetClient().Self.AlertMessage += BotAlertMessage;
|
GetClient().Self.AlertMessage += BotAlertMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,7 @@ namespace SecondBotEvents.Services
|
||||||
"discordapi", "GroupBan", null, [.. args], 0, "none", false, 0, "", false), message);
|
"discordapi", "GroupBan", null, [.. args], 0, "none", false, 0, "", false), message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
string groupSender = "{BOT} " + (myConfig.GethideChatterName() ? GetClient().Self.Name : message.Author.Username);
|
||||||
if (myConfig.GethideChatterName() == false)
|
if (myConfig.GethideChatterName() == false)
|
||||||
{
|
{
|
||||||
GetClient().Self.InstantMessageGroup(groupuuid, message.Author.Username + ": " + message.CleanContent);
|
GetClient().Self.InstantMessageGroup(groupuuid, message.Author.Username + ": " + message.CleanContent);
|
||||||
|
|
@ -366,6 +367,7 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
GetClient().Self.InstantMessageGroup(groupuuid, message.CleanContent);
|
GetClient().Self.InstantMessageGroup(groupuuid, message.CleanContent);
|
||||||
}
|
}
|
||||||
|
master.DataStoreService.RecordGroupChat(groupuuid, groupSender, message.CleanContent);
|
||||||
_ = message.DeleteAsync();
|
_ = message.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -444,6 +446,7 @@ namespace SecondBotEvents.Services
|
||||||
GetClient().Self.SendTeleportLure(avataruuid);
|
GetClient().Self.SendTeleportLure(avataruuid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
string botSender = "{BOT} " + (myConfig.GethideChatterName() ? GetClient().Self.Name : message.Author.Username);
|
||||||
if (myConfig.GethideChatterName() == false)
|
if (myConfig.GethideChatterName() == false)
|
||||||
{
|
{
|
||||||
master.DataStoreService.GetAvatarName(avataruuid); // add avatar to lookup service just in case.
|
master.DataStoreService.GetAvatarName(avataruuid); // add avatar to lookup service just in case.
|
||||||
|
|
@ -453,6 +456,7 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
GetClient().Self.InstantMessage(avataruuid, message.CleanContent);
|
GetClient().Self.InstantMessage(avataruuid, message.CleanContent);
|
||||||
}
|
}
|
||||||
|
master.DataStoreService.RecordAvatarIm(avataruuid, botSender, message.CleanContent);
|
||||||
_ = MarkMessage((IUserMessage)message, "✅");
|
_ = MarkMessage((IUserMessage)message, "✅");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -819,9 +823,10 @@ namespace SecondBotEvents.Services
|
||||||
SendMessageToChannel(groupname, GroupPrefill(group), name + ": " + message, "group");
|
SendMessageToChannel(groupname, GroupPrefill(group), name + ": " + message, "group");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AvatarIMChat(UUID avatar, string name, string message)
|
public void AvatarIMChat(UUID avatar, string name, string message, string sender = null)
|
||||||
{
|
{
|
||||||
SendMessageToChannel(name.ToLower().Replace(" ",""), "avatar=" + avatar.ToString() + "||| !close !offertp !clear", name+":"+message, "im");
|
string displaySender = string.IsNullOrEmpty(sender) ? name : sender;
|
||||||
|
SendMessageToChannel(name.ToLower().Replace(" ",""), "avatar=" + avatar.ToString() + "||| !close !offertp !clear", displaySender + ": " + message, "im");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ObjectIMChat(string objectName, string message)
|
protected void ObjectIMChat(string objectName, string message)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using Discord.Rest;
|
using Discord.Rest;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
using SecondBotEvents.Config;
|
using SecondBotEvents.Config;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SecondBotEvents.Services
|
namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
|
|
@ -25,6 +26,7 @@ namespace SecondBotEvents.Services
|
||||||
protected bool softDisable = false;
|
protected bool softDisable = false;
|
||||||
protected bool closeToHome = false;
|
protected bool closeToHome = false;
|
||||||
protected bool softDisableSit = false;
|
protected bool softDisableSit = false;
|
||||||
|
protected Timer? _homeboundTimer = null;
|
||||||
|
|
||||||
public void MarkTeleport()
|
public void MarkTeleport()
|
||||||
{
|
{
|
||||||
|
|
@ -334,19 +336,27 @@ namespace SecondBotEvents.Services
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Stop();
|
Stop();
|
||||||
if(master.RLV.Enabled == true)
|
|
||||||
{
|
|
||||||
myConfig.setEnabled(false);
|
|
||||||
LogFormater.Info("Homebound Service [Disabled RLV enabled]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
running = true;
|
running = true;
|
||||||
master.BotClientNoticeEvent += BotClientRestart;
|
master.BotClientNoticeEvent += BotClientRestart;
|
||||||
|
_homeboundTimer?.Dispose();
|
||||||
|
_homeboundTimer = new Timer((state) =>
|
||||||
|
{
|
||||||
|
if (running && botConnected && softDisable == false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}, null, 5000, 5000);
|
||||||
LogFormater.Info("Homebound Service [Starting]");
|
LogFormater.Info("Homebound Service [Starting]");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Stop()
|
public override void Stop()
|
||||||
{
|
{
|
||||||
|
_homeboundTimer?.Dispose();
|
||||||
|
_homeboundTimer = null;
|
||||||
if(running == true)
|
if(running == true)
|
||||||
{
|
{
|
||||||
LogFormater.Info("Homebound Service [Stopping]");
|
LogFormater.Info("Homebound Service [Stopping]");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,27 @@ namespace SecondBotEvents.Services
|
||||||
{
|
{
|
||||||
protected new InteractionConfig myConfig;
|
protected new InteractionConfig myConfig;
|
||||||
protected bool botConnected = false;
|
protected bool botConnected = false;
|
||||||
|
private readonly Dictionary<UUID, (string SenderName, UUID SenderID)> _pendingOffers = [];
|
||||||
|
|
||||||
|
public UUID GetOrCreatePendingFolder()
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
if (client == null || client.Inventory == null || client.Inventory.Store == null || client.Inventory.Store.RootFolder == null)
|
||||||
|
{
|
||||||
|
return UUID.Zero;
|
||||||
|
}
|
||||||
|
var store = client.Inventory.Store;
|
||||||
|
var rootContent = store.GetContents(store.RootFolder.UUID);
|
||||||
|
foreach (var baseItem in rootContent)
|
||||||
|
{
|
||||||
|
if (baseItem is InventoryFolder folder && folder.Name.Equals("#Pending Approval", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return folder.UUID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client.Inventory.CreateFolder(store.RootFolder.UUID, "#Pending Approval");
|
||||||
|
}
|
||||||
|
|
||||||
public InteractionService(EventsSecondBot setMaster) : base(setMaster)
|
public InteractionService(EventsSecondBot setMaster) : base(setMaster)
|
||||||
{
|
{
|
||||||
myConfig = new InteractionConfig(master.fromEnv,master.fromFolder);
|
myConfig = new InteractionConfig(master.fromEnv,master.fromFolder);
|
||||||
|
|
@ -70,17 +91,22 @@ namespace SecondBotEvents.Services
|
||||||
string ObjectOwnerName = master.DataStoreService.GetAvatarName(avataruuid);
|
string ObjectOwnerName = master.DataStoreService.GetAvatarName(avataruuid);
|
||||||
string whyAccepted = "rejected request";
|
string whyAccepted = "rejected request";
|
||||||
misc.Add("ObjectOwner", ObjectOwnerName);
|
misc.Add("ObjectOwner", ObjectOwnerName);
|
||||||
if (master.CommandsService.myConfig.GetMastersCSV().Contains(avatarName) == true)
|
foreach (string masterName in master.CommandsService.myConfig.GetMastersCSV())
|
||||||
{
|
{
|
||||||
whyAccepted = "on owners list";
|
if (SecondbotHelpers.IsNameMatch(masterName, avatarName))
|
||||||
accepted = true;
|
{
|
||||||
|
whyAccepted = "on owners list";
|
||||||
|
accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (SecondbotHelpers.IsNameMatch(masterName, ObjectOwnerName))
|
||||||
|
{
|
||||||
|
whyAccepted = "Object Owner is on master list";
|
||||||
|
accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (master.CommandsService.myConfig.GetMastersCSV().Contains(ObjectOwnerName) == true)
|
if (ConfigAcceptLevel == "Anyone")
|
||||||
{
|
|
||||||
whyAccepted = "Object Owner is on master list";
|
|
||||||
accepted = true;
|
|
||||||
}
|
|
||||||
else if (ConfigAcceptLevel == "Anyone")
|
|
||||||
{
|
{
|
||||||
whyAccepted = "Accepting from anyone";
|
whyAccepted = "Accepting from anyone";
|
||||||
accepted = true;
|
accepted = true;
|
||||||
|
|
@ -95,10 +121,15 @@ namespace SecondBotEvents.Services
|
||||||
string[] bits = avatarName.ToLower().Split('.');
|
string[] bits = avatarName.ToLower().Split('.');
|
||||||
if (bits.Length == 2)
|
if (bits.Length == 2)
|
||||||
{
|
{
|
||||||
accepted = master.CommandsService.myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
|
string dotName = bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper();
|
||||||
if (accepted == true)
|
foreach (string masterName in master.CommandsService.myConfig.GetMastersCSV())
|
||||||
{
|
{
|
||||||
whyAccepted = "on owners list [dot check]";
|
if (SecondbotHelpers.IsNameMatch(masterName, dotName))
|
||||||
|
{
|
||||||
|
accepted = true;
|
||||||
|
whyAccepted = "on owners list [dot check]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (accepted == false)
|
if (accepted == false)
|
||||||
|
|
@ -106,10 +137,15 @@ namespace SecondBotEvents.Services
|
||||||
bits = ObjectOwnerName.ToLower().Split('.');
|
bits = ObjectOwnerName.ToLower().Split('.');
|
||||||
if (bits.Length == 2)
|
if (bits.Length == 2)
|
||||||
{
|
{
|
||||||
accepted = master.CommandsService.myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
|
string dotName = bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper();
|
||||||
if (accepted == true)
|
foreach (string masterName in master.CommandsService.myConfig.GetMastersCSV())
|
||||||
{
|
{
|
||||||
whyAccepted = "Object Owner is on master list [dot check]";
|
if (SecondbotHelpers.IsNameMatch(masterName, dotName))
|
||||||
|
{
|
||||||
|
accepted = true;
|
||||||
|
whyAccepted = "Object Owner is on master list [dot check]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +285,26 @@ namespace SecondBotEvents.Services
|
||||||
protected void BotInventoryAdd(object o, InventoryObjectAddedEventArgs e)
|
protected void BotInventoryAdd(object o, InventoryObjectAddedEventArgs e)
|
||||||
{
|
{
|
||||||
InventoryItem F = (InventoryItem)e.Obj;
|
InventoryItem F = (InventoryItem)e.Obj;
|
||||||
|
UUID pendingFolder = GetOrCreatePendingFolder();
|
||||||
|
if (F.ParentUUID == pendingFolder)
|
||||||
|
{
|
||||||
|
(string SenderName, UUID SenderID) senderDetails = ("Unknown", UUID.Zero);
|
||||||
|
bool found = false;
|
||||||
|
lock (_pendingOffers)
|
||||||
|
{
|
||||||
|
if (_pendingOffers.TryGetValue(F.UUID, out senderDetails))
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
_pendingOffers.Remove(F.UUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
F.Description = $"Sent by: {senderDetails.SenderName} ({senderDetails.SenderID})";
|
||||||
|
GetClient().Inventory.RequestUpdateItem(F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary<string, string> details = new()
|
Dictionary<string, string> details = new()
|
||||||
{
|
{
|
||||||
{ "itemname", e.Obj.Name },
|
{ "itemname", e.Obj.Name },
|
||||||
|
|
@ -264,17 +320,66 @@ namespace SecondBotEvents.Services
|
||||||
|
|
||||||
protected void BotInventoryOffer(object o, InventoryObjectOfferedEventArgs e)
|
protected void BotInventoryOffer(object o, InventoryObjectOfferedEventArgs e)
|
||||||
{
|
{
|
||||||
|
UUID ownerID = e.Offer.FromAgentID;
|
||||||
|
string ownerName = e.Offer.FromAgentName;
|
||||||
|
|
||||||
|
if (e.FromTask)
|
||||||
|
{
|
||||||
|
var sim = GetClient().Network.CurrentSim;
|
||||||
|
var prim = sim?.ObjectsPrimitives.Values.FirstOrDefault(p => p.ID == e.Offer.FromAgentID);
|
||||||
|
if (prim != null)
|
||||||
|
{
|
||||||
|
ownerID = prim.OwnerID;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.Threading.AutoResetEvent propertiesEvent = new(false);
|
||||||
|
EventHandler<ObjectPropertiesFamilyEventArgs> handler = (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Properties.ObjectID == e.Offer.FromAgentID)
|
||||||
|
{
|
||||||
|
ownerID = args.Properties.OwnerID;
|
||||||
|
propertiesEvent.Set();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
GetClient().Objects.ObjectPropertiesFamily += handler;
|
||||||
|
GetClient().Objects.RequestObjectPropertiesFamily(GetClient().Network.CurrentSim, e.Offer.FromAgentID);
|
||||||
|
propertiesEvent.WaitOne(1000);
|
||||||
|
GetClient().Objects.ObjectPropertiesFamily -= handler;
|
||||||
|
}
|
||||||
|
ownerName = master.DataStoreService.GetAvatarName(ownerID);
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary<string, string> details = new()
|
Dictionary<string, string> details = new()
|
||||||
{
|
{
|
||||||
{ "transactionid", e.Offer.IMSessionID.ToString() },
|
{ "transactionid", e.Offer.IMSessionID.ToString() },
|
||||||
{ "itemtype", e.AssetType.ToString() },
|
{ "itemtype", e.AssetType.ToString() },
|
||||||
{ "message", e.Offer.Message.ToString() },
|
{ "message", e.Offer.Message.ToString() },
|
||||||
{ "fromscript", e.FromTask.ToString() },
|
{ "fromscript", e.FromTask.ToString() },
|
||||||
{ "targetfolder", e.FolderID.ToString() }
|
{ "targetfolder", e.FolderID.ToString() },
|
||||||
|
{ "objectid", e.Offer.FromAgentID.ToString() },
|
||||||
|
{ "objectname", e.Offer.FromAgentName }
|
||||||
};
|
};
|
||||||
if (ProcessRequest(myConfig.GetAcceptInventory(),myConfig.GetInventoryTransferLevel(),e.Offer.FromAgentName, "InventoryOffer", e.Offer.FromAgentID, details) == false)
|
if (ProcessRequest(myConfig.GetAcceptInventory(),myConfig.GetInventoryTransferLevel(), ownerName, "InventoryOffer", ownerID, details) == false)
|
||||||
{
|
{
|
||||||
e.Accept = false;
|
if (myConfig.GetAcceptInventory())
|
||||||
|
{
|
||||||
|
UUID pendingFolder = GetOrCreatePendingFolder();
|
||||||
|
e.FolderID = pendingFolder;
|
||||||
|
e.Accept = true;
|
||||||
|
lock (_pendingOffers)
|
||||||
|
{
|
||||||
|
_pendingOffers[e.ObjectID] = (ownerName, ownerID);
|
||||||
|
}
|
||||||
|
details.Add("pending", "true");
|
||||||
|
details.Add("itemuuid", e.ObjectID.ToString());
|
||||||
|
details.Add("itemname", "?");
|
||||||
|
JsonOuputCleaner(details, "InventoryOffer");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.Accept = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.Accept = true;
|
e.Accept = true;
|
||||||
|
|
@ -323,6 +428,7 @@ namespace SecondBotEvents.Services
|
||||||
protected async void BotLoggedIn(object o, SimConnectedEventArgs e)
|
protected async void BotLoggedIn(object o, SimConnectedEventArgs e)
|
||||||
{
|
{
|
||||||
await awaitstable();
|
await awaitstable();
|
||||||
|
GetOrCreatePendingFolder();
|
||||||
GetClient().Network.SimConnected -= BotLoggedIn;
|
GetClient().Network.SimConnected -= BotLoggedIn;
|
||||||
GetClient().Friends.FriendshipOffered += BotFriendRequested;
|
GetClient().Friends.FriendshipOffered += BotFriendRequested;
|
||||||
GetClient().Inventory.InventoryObjectOffered += BotInventoryOffer;
|
GetClient().Inventory.InventoryObjectOffered += BotInventoryOffer;
|
||||||
|
|
@ -364,12 +470,19 @@ namespace SecondBotEvents.Services
|
||||||
master.BotClientNoticeEvent -= BotClientRestart;
|
master.BotClientNoticeEvent -= BotClientRestart;
|
||||||
if (master.BotClient != null)
|
if (master.BotClient != null)
|
||||||
{
|
{
|
||||||
if (GetClient() != null)
|
var client = GetClient();
|
||||||
|
if (client != null)
|
||||||
{
|
{
|
||||||
GetClient().Friends.FriendshipOffered -= BotFriendRequested;
|
if (client.Friends != null)
|
||||||
GetClient().Inventory.InventoryObjectOffered -= BotInventoryOffer;
|
client.Friends.FriendshipOffered -= BotFriendRequested;
|
||||||
GetClient().Inventory.Store.InventoryObjectAdded -= BotInventoryAdd;
|
if (client.Inventory != null)
|
||||||
GetClient().Groups.GroupInvitation -= BotGroupInviteOffer;
|
{
|
||||||
|
client.Inventory.InventoryObjectOffered -= BotInventoryOffer;
|
||||||
|
if (client.Inventory.Store != null)
|
||||||
|
client.Inventory.Store.InventoryObjectAdded -= BotInventoryAdd;
|
||||||
|
}
|
||||||
|
if (client.Groups != null)
|
||||||
|
client.Groups.GroupInvitation -= BotGroupInviteOffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -180,5 +180,20 @@ namespace SecondBotEvents
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsNameMatch(string configuredName, string incomingName)
|
||||||
|
{
|
||||||
|
if (configuredName == null || incomingName == null) return false;
|
||||||
|
|
||||||
|
string c = configuredName.Trim().ToLowerInvariant();
|
||||||
|
string i = incomingName.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
if (c == i) return true;
|
||||||
|
|
||||||
|
if (c.EndsWith(" resident")) c = c.Substring(0, c.Length - 9).Trim();
|
||||||
|
if (i.EndsWith(" resident")) i = i.Substring(0, i.Length - 9).Trim();
|
||||||
|
|
||||||
|
return c == i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue