Update SecondBot services, commands (Info, Inventory, Interaction), and DataStore enhancements
Some checks are pending
autobuild / docker (push) Waiting to run

This commit is contained in:
RebeccaNod1 2026-11-04 17:03:11 -07:00
parent f9094f6fca
commit 0b081d9e1f
16 changed files with 2624 additions and 90 deletions

View file

@ -1334,7 +1334,7 @@ namespace OpenMetaverse
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)
{

View file

@ -181,7 +181,7 @@ namespace OpenMetaverse
/// <summary>Enable/disable storing terrain heightmaps in the
/// TerrainManager</summary>
public bool STORE_LAND_PATCHES = false;
public bool STORE_LAND_PATCHES = true;
/// <summary>Enable/disable sending agent updates</summary>
public bool SEND_AGENT_UPDATES = true;
@ -208,7 +208,7 @@ namespace OpenMetaverse
/// <summary>Should we connect to multiple sims? This will allow
/// viewing into neighboring simulators and sim crossings
/// (Experimental)</summary>
public bool MULTIPLE_SIMS = false;
public bool MULTIPLE_SIMS = true;
/// <summary>If true, all object update packets will be decoded in to
/// native objects. If false, only updates for our own agent will be

View file

@ -80,11 +80,27 @@ namespace SecondBotEvents.Commands
List<NearMeDetails> BetterNearMe = [];
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);
float offsetX = (long)simX - (long)mySimX;
float offsetY = (long)simY - (long)mySimY;
HashSet<UUID> processedAvatars = [];
List<Avatar> avcopy = [.. sim.ObjectsAvatars.Values];
@ -100,21 +116,20 @@ namespace SecondBotEvents.Commands
localPos = parentObj.Position + (av.Position * parentObj.Rotation);
}
Vector3 globalPos = new Vector3(simX + localPos.X, simY + localPos.Y, localPos.Z);
double dist = Math.Round(Vector3.Distance(globalPos, myGlobalPos), 2);
float relX = offsetX + localPos.X;
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;
float relX = (simX - mySimX) + localPos.X;
float relY = (simY - mySimY) + localPos.Y;
NearMeDetails details = new()
{
id = av.ID.ToString(),
name = av.Name,
x = (int)Math.Round(relX),
y = (int)Math.Round(relY),
z = (int)Math.Round(globalPos.Z),
z = (int)Math.Round(localPos.Z),
range = dist
};
BetterNearMe.Add(details);
@ -126,14 +141,13 @@ namespace SecondBotEvents.Commands
if (avID == GetClient().Self.AgentID || processedAvatars.Contains(avID)) continue;
Vector3 localPos = kvp.Value;
Vector3 globalPos = new Vector3(simX + localPos.X, simY + localPos.Y, localPos.Z);
double dist = Math.Round(Vector3.Distance(globalPos, myGlobalPos), 2);
float relX = offsetX + localPos.X;
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;
float relX = (simX - mySimX) + localPos.X;
float relY = (simY - mySimY) + localPos.Y;
string avName = master.DataStoreService.GetAvatarName(avID);
if (avName == "lookup" || string.IsNullOrEmpty(avName)) avName = "Unknown";
@ -143,7 +157,7 @@ namespace SecondBotEvents.Commands
name = avName,
x = (int)Math.Round(relX),
y = (int)Math.Round(relY),
z = (int)Math.Round(globalPos.Z),
z = (int)Math.Round(localPos.Z),
range = dist
};
BetterNearMe.Add(details);

View file

@ -98,6 +98,17 @@ namespace SecondBotEvents.Commands
}
master.DataStoreService.OpenChatWindow(false, avataruuid, avataruuid);
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]);
}

View file

@ -495,6 +495,8 @@ namespace SecondBotEvents.Commands
return Failure("Unknown group", [group]);
}
GetClient().Self.InstantMessageGroup(groupUUID, message);
string senderName = "{BOT} " + GetClient().Self.Name;
master.DataStoreService.RecordGroupChat(groupUUID, senderName, message);
return BasicReply("Sending");
}
[About("fetchs a list of all pending group invitations")]

File diff suppressed because it is too large Load diff

View file

@ -1301,6 +1301,184 @@ namespace SecondBotEvents.Commands
}
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

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using System;
using System.Text.Json;
using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Services;
@ -273,6 +274,41 @@ namespace SecondBotEvents.Commands
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]);
}
}

View file

@ -166,8 +166,12 @@ private BotRejoinConfig rejoinCfg;
protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e)
{
goodticks = 0;
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())
{
HandleUnexpectedDisconnect("Unexpected sim disconnect");
@ -290,6 +294,8 @@ private BotRejoinConfig rejoinCfg;
{
client = null;
client = new GridClient();
client.Settings.STORE_LAND_PATCHES = true;
client.Settings.MULTIPLE_SIMS = true;
client.Network.SimConnected += BotSimConnected;
client.Network.LoggedOut += BotLoggedOut;
client.Network.Disconnected += BotDisconnected;

View file

@ -145,13 +145,13 @@ namespace SecondBotEvents.Services
if(myConfig.GetObjectMasterOptout() == true)
{
string ownername = master.DataStoreService.GetAvatarName(e.IM.FromAgentID);
requireSigning = !myConfig.GetMastersCSV().Contains(ownername);
if ((requireSigning == false) && (myConfig.GetCheckDotNames() == true))
requireSigning = !IsMaster(ownername);
if (requireSigning && myConfig.GetCheckDotNames())
{
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
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);
requireSigning = false;
acceptMessage = myConfig.GetMastersCSV().Contains(e.IM.FromAgentName);
acceptMessage = IsMaster(e.IM.FromAgentName);
if ((acceptMessage == false) && (myConfig.GetCheckDotNames() == true))
{
string[] bits = e.IM.FromAgentName.ToLower().Split('.');
if (bits.Length == 2)
{
acceptMessage = myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper()+" " + bits[1].FirstCharToUpper());
acceptMessage = IsMaster(bits[0].FirstCharToUpper()+" " + bits[1].FirstCharToUpper());
}
}
break;
@ -193,20 +193,26 @@ namespace SecondBotEvents.Services
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 (find != "lookup")
if (SecondbotHelpers.IsNameMatch(masterName, name))
{
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;
}

View file

@ -221,6 +221,10 @@ namespace SecondBotEvents.Services
{
continue;
}
if (chatWindowsOwner.ContainsValue(entry.Value) == true)
{
continue;
}
clearLinks.Add(entry.Value, entry.Key);
}
lock (avatarLastUsed) lock (avatarsName2Key) lock(avatarsKey2Displayname)
@ -406,25 +410,167 @@ namespace SecondBotEvents.Services
public string GetGroupMemberRank(UUID group, UUID avatar)
{
bool needFetch = false;
lock (groupMembers)
{
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";
}
GroupMember m = groupMembers[group][avatar];
if (m.IsOwner == true) return "Owner";
if (m.Title != null && (m.Title.Contains("Officer") || m.Title.Contains("Officer"))) return "Officer";
// Also check roles if title didn't match
if (groupRoles.ContainsKey(group))
// Check for Owner powers (DeleteRole, ChangeActions, ChangeIdentity)
GroupPowers ownerPowers = GroupPowers.DeleteRole | GroupPowers.ChangeActions | GroupPowers.ChangeIdentity;
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
// For now, Owner and Title check is a good start.
if (m.Title.Contains("Owner", StringComparison.OrdinalIgnoreCase) || m.Title.Contains("Founder", StringComparison.OrdinalIgnoreCase))
{
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)
@ -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)
{
lock (chatWindows)
@ -862,6 +1042,7 @@ namespace SecondBotEvents.Services
{
if (av.Key == avatarId)
{
AddAvatar(av.Key, av.Value);
hasReply = true;
fetchEvent.Set();
}
@ -918,15 +1099,11 @@ namespace SecondBotEvents.Services
public bool KnownAvatar(string name, UUID avatarId)
{
SyncAvatarsHashMap();
if (avatarId == UUID.Zero)
{
return false;
}
if(KnownAvatarUUID(avatarId) == true)
if (avatarId != UUID.Zero && KnownAvatarUUID(avatarId) == true)
{
return true;
}
if(KnownAvatarName(name) == true)
if (!string.IsNullOrEmpty(name) && KnownAvatarName(name) == true)
{
return true;
}
@ -1829,21 +2006,93 @@ namespace SecondBotEvents.Services
displayNameFinder(id);
return;
}
avatarsName2Key.Add(name, id);
avatarLastUsed.Add(id, SecondbotHelpers.UnixTimeNow());
avatarsKey2Displayname.Add(id, "?");
avatarsName2Key[name] = id;
avatarLastUsed[id] = SecondbotHelpers.UnixTimeNow();
if (avatarsKey2Displayname.ContainsKey(id) == false)
{
avatarsKey2Displayname.Add(id, "?");
}
displayNameFinder(id);
}
}
public string GetDisplayName(UUID id)
{
requestDisplayName(id);
if (avatarsKey2Displayname.ContainsKey(id) == false)
if (avatarsKey2Displayname.ContainsKey(id) && avatarsKey2Displayname[id] != "?" && !string.IsNullOrEmpty(avatarsKey2Displayname[id]))
{
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)

View file

@ -195,6 +195,18 @@ namespace SecondBotEvents.Services
{
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;
nextDialogWindow++;
if(nextDialogWindow > 999)
@ -251,13 +263,14 @@ namespace SecondBotEvents.Services
return;
}
LogFormater.Info("Dialog service [Attached to new client]");
GetClient().Network.LoggedOut -= BotLoggedOut;
GetClient().Network.LoggedOut += BotLoggedOut;
GetClient().Network.SimConnected -= BotLoggedIn;
GetClient().Network.SimConnected += BotLoggedIn;
}
protected void BotLoggedOut(object o, LoggedOutEventArgs e)
{
GetClient().Network.SimConnected += BotLoggedIn;
LogFormater.Info("Dialog service [waiting for new client]");
if (GetClient() != null)
{
@ -271,18 +284,24 @@ namespace SecondBotEvents.Services
{
GetClient().Network.SimConnected -= BotLoggedIn;
botConnected = true;
// attach events
// attach events cleanly
GetClient().Self.ScriptDialog -= DialogWindowEvent;
GetClient().Self.ScriptDialog += DialogWindowEvent;
GetClient().Self.LoadURL -= LoadUrlEvent;
GetClient().Self.LoadURL += LoadUrlEvent;
GetClient().Self.AlertMessage -= BotAlertMessage;
GetClient().Self.AlertMessage += BotAlertMessage;
}
public void Attach()
{
botConnected = true;
// attach events
// attach events cleanly
GetClient().Self.ScriptDialog -= DialogWindowEvent;
GetClient().Self.ScriptDialog += DialogWindowEvent;
GetClient().Self.LoadURL -= LoadUrlEvent;
GetClient().Self.LoadURL += LoadUrlEvent;
GetClient().Self.AlertMessage -= BotAlertMessage;
GetClient().Self.AlertMessage += BotAlertMessage;
}

View file

@ -358,6 +358,7 @@ namespace SecondBotEvents.Services
"discordapi", "GroupBan", null, [.. args], 0, "none", false, 0, "", false), message);
return;
}
string groupSender = "{BOT} " + (myConfig.GethideChatterName() ? GetClient().Self.Name : message.Author.Username);
if (myConfig.GethideChatterName() == false)
{
GetClient().Self.InstantMessageGroup(groupuuid, message.Author.Username + ": " + message.CleanContent);
@ -366,6 +367,7 @@ namespace SecondBotEvents.Services
{
GetClient().Self.InstantMessageGroup(groupuuid, message.CleanContent);
}
master.DataStoreService.RecordGroupChat(groupuuid, groupSender, message.CleanContent);
_ = message.DeleteAsync();
}
@ -444,6 +446,7 @@ namespace SecondBotEvents.Services
GetClient().Self.SendTeleportLure(avataruuid);
return;
}
string botSender = "{BOT} " + (myConfig.GethideChatterName() ? GetClient().Self.Name : message.Author.Username);
if (myConfig.GethideChatterName() == false)
{
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);
}
master.DataStoreService.RecordAvatarIm(avataruuid, botSender, message.CleanContent);
_ = MarkMessage((IUserMessage)message, "✅");
}
@ -819,9 +823,10 @@ namespace SecondBotEvents.Services
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)

View file

@ -1,8 +1,9 @@
using Discord.Rest;
using Discord.Rest;
using OpenMetaverse;
using SecondBotEvents.Config;
using System;
using System.Linq;
using System.Threading;
namespace SecondBotEvents.Services
{
@ -25,6 +26,7 @@ namespace SecondBotEvents.Services
protected bool softDisable = false;
protected bool closeToHome = false;
protected bool softDisableSit = false;
protected Timer? _homeboundTimer = null;
public void MarkTeleport()
{
@ -334,19 +336,27 @@ namespace SecondBotEvents.Services
return;
}
Stop();
if(master.RLV.Enabled == true)
{
myConfig.setEnabled(false);
LogFormater.Info("Homebound Service [Disabled RLV enabled]");
return;
}
running = true;
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]");
}
public override void Stop()
{
_homeboundTimer?.Dispose();
_homeboundTimer = null;
if(running == true)
{
LogFormater.Info("Homebound Service [Stopping]");

View file

@ -13,6 +13,27 @@ namespace SecondBotEvents.Services
{
protected new InteractionConfig myConfig;
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)
{
myConfig = new InteractionConfig(master.fromEnv,master.fromFolder);
@ -70,17 +91,22 @@ namespace SecondBotEvents.Services
string ObjectOwnerName = master.DataStoreService.GetAvatarName(avataruuid);
string whyAccepted = "rejected request";
misc.Add("ObjectOwner", ObjectOwnerName);
if (master.CommandsService.myConfig.GetMastersCSV().Contains(avatarName) == true)
foreach (string masterName in master.CommandsService.myConfig.GetMastersCSV())
{
whyAccepted = "on owners list";
accepted = true;
if (SecondbotHelpers.IsNameMatch(masterName, avatarName))
{
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)
{
whyAccepted = "Object Owner is on master list";
accepted = true;
}
else if (ConfigAcceptLevel == "Anyone")
if (ConfigAcceptLevel == "Anyone")
{
whyAccepted = "Accepting from anyone";
accepted = true;
@ -95,10 +121,15 @@ namespace SecondBotEvents.Services
string[] bits = avatarName.ToLower().Split('.');
if (bits.Length == 2)
{
accepted = master.CommandsService.myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
if (accepted == true)
string dotName = bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper();
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)
@ -106,10 +137,15 @@ namespace SecondBotEvents.Services
bits = ObjectOwnerName.ToLower().Split('.');
if (bits.Length == 2)
{
accepted = master.CommandsService.myConfig.GetMastersCSV().Contains(bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper());
if (accepted == true)
string dotName = bits[0].FirstCharToUpper() + " " + bits[1].FirstCharToUpper();
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)
{
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()
{
{ "itemname", e.Obj.Name },
@ -264,17 +320,66 @@ namespace SecondBotEvents.Services
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()
{
{ "transactionid", e.Offer.IMSessionID.ToString() },
{ "itemtype", e.AssetType.ToString() },
{ "message", e.Offer.Message.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;
}
e.Accept = true;
@ -323,6 +428,7 @@ namespace SecondBotEvents.Services
protected async void BotLoggedIn(object o, SimConnectedEventArgs e)
{
await awaitstable();
GetOrCreatePendingFolder();
GetClient().Network.SimConnected -= BotLoggedIn;
GetClient().Friends.FriendshipOffered += BotFriendRequested;
GetClient().Inventory.InventoryObjectOffered += BotInventoryOffer;
@ -364,12 +470,19 @@ namespace SecondBotEvents.Services
master.BotClientNoticeEvent -= BotClientRestart;
if (master.BotClient != null)
{
if (GetClient() != null)
var client = GetClient();
if (client != null)
{
GetClient().Friends.FriendshipOffered -= BotFriendRequested;
GetClient().Inventory.InventoryObjectOffered -= BotInventoryOffer;
GetClient().Inventory.Store.InventoryObjectAdded -= BotInventoryAdd;
GetClient().Groups.GroupInvitation -= BotGroupInviteOffer;
if (client.Friends != null)
client.Friends.FriendshipOffered -= BotFriendRequested;
if (client.Inventory != null)
{
client.Inventory.InventoryObjectOffered -= BotInventoryOffer;
if (client.Inventory.Store != null)
client.Inventory.Store.InventoryObjectAdded -= BotInventoryAdd;
}
if (client.Groups != null)
client.Groups.GroupInvitation -= BotGroupInviteOffer;
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -180,5 +180,20 @@ namespace SecondBotEvents
}
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;
}
}
}