Dashboard & Group Management Integration

- [LibreMetaverse] Enhanced GroupInvitationEventArgs with GroupID.
- [InteractionService] Added persistence for group invites when auto-accept is disabled.
- [DataStoreService] Implemented pending group invitation tracking and management.
- [DataStoreService] Enhanced local chat recording with Shout/Whisper/OwnerSay type tags.
- [BotClientService] Improved logout stability with explicit logout call and delay.
- [RLVService] Exposed active RLV rules list for external monitoring.
- [Commands] Added GroupInviteRespond, GetPendingGroupInvites, and RLVStatus API commands.
- [Commands] Overhauled NearmeWithDetails to support multi-simulator tracking and better position resolution for seated avatars.
This commit is contained in:
RebeccaNod1 2026-05-05 16:01:58 -06:00
parent 176b9d059f
commit adf4e30cd2
10 changed files with 196 additions and 27 deletions

View file

@ -914,7 +914,7 @@ namespace OpenMetaverse
{
if (m_GroupInvitation == null || e.IM.Dialog != InstantMessageDialog.GroupInvitation) return;
GroupInvitationEventArgs args = new GroupInvitationEventArgs(e.Simulator, e.IM.FromAgentID, e.IM.FromAgentName, e.IM.Message);
GroupInvitationEventArgs args = new GroupInvitationEventArgs(e.Simulator, e.IM.FromAgentID, e.IM.FromAgentName, e.IM.Message, e.IM.IMSessionID);
OnGroupInvitation(args);
if (args.Accept)
@ -2460,15 +2460,19 @@ namespace OpenMetaverse
/// <summary>The Simulator</summary>
public Simulator Simulator { get; }
/// <summary>The Group ID</summary>
public UUID GroupID { get; }
/// <summary>Set to true to accept invitation, false to decline</summary>
public bool Accept { get; set; }
public GroupInvitationEventArgs(Simulator simulator, UUID agentID, string agentName, string message)
public GroupInvitationEventArgs(Simulator simulator, UUID agentID, string agentName, string message, UUID groupID)
{
Simulator = simulator;
AgentID = agentID;
FromName = agentName;
Message = message;
GroupID = groupID;
}
}

View file

@ -78,25 +78,73 @@ namespace SecondBotEvents.Commands
return BasicReply("Error not in a sim");
}
List<NearMeDetails> BetterNearMe = [];
List<Avatar> avcopy = [.. GetClient().Network.CurrentSim.ObjectsAvatars.Values];
foreach (Avatar av in avcopy)
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);
foreach (var sim in GetClient().Network.Simulators)
{
if (av.ID != GetClient().Self.AgentID)
Utils.LongToUInts(sim.Handle, out uint simX, out uint simY);
HashSet<UUID> processedAvatars = [];
List<Avatar> avcopy = [.. sim.ObjectsAvatars.Values];
foreach (Avatar av in avcopy)
{
Vector3 globalPos = av.Position;
if (av.ParentID != 0 && GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(av.ParentID, out Primitive parentObj))
if (av.ID == GetClient().Self.AgentID) continue;
processedAvatars.Add(av.ID);
Vector3 localPos = av.Position;
if (av.ParentID != 0 && sim.ObjectsPrimitives.TryGetValue(av.ParentID, out Primitive parentObj))
{
globalPos = 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);
double dist = Math.Round(Vector3.Distance(globalPos, myGlobalPos), 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(globalPos.X),
y = (int)Math.Round(globalPos.Y),
x = (int)Math.Round(relX),
y = (int)Math.Round(relY),
z = (int)Math.Round(globalPos.Z),
range = Math.Round(Vector3.Distance(globalPos, GetClient().Self.SimPosition), 2)
range = dist
};
BetterNearMe.Add(details);
}
foreach (var kvp in sim.AvatarPositions)
{
UUID avID = kvp.Key;
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);
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";
NearMeDetails details = new()
{
id = avID.ToString(),
name = avName,
x = (int)Math.Round(relX),
y = (int)Math.Round(relY),
z = (int)Math.Round(globalPos.Z),
range = dist
};
BetterNearMe.Add(details);
}

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Services;
@ -31,7 +31,8 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild channel", [channel, message]);
}
master.DataStoreService.BotRecordLocalchatReply(message);
string typeTag = (channelnum == 0) ? "" : $"[Channel {channelnum}] ";
master.DataStoreService.BotRecordLocalchatReply(message, typeTag);
GetClient().Self.Chat(message, channelnum, ChatType.Normal);
return BasicReply("ok");
}
@ -52,7 +53,7 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild channel", [channel, message]);
}
master.DataStoreService.BotRecordLocalchatReply(message);
master.DataStoreService.BotRecordLocalchatReply(message, "[Shout] ");
GetClient().Self.Chat(message, channelnum, ChatType.Shout);
return BasicReply("ok");
}
@ -73,7 +74,7 @@ namespace SecondBotEvents.Commands
{
return Failure("Invaild channel", [channel, message]);
}
master.DataStoreService.BotRecordLocalchatReply(message);
master.DataStoreService.BotRecordLocalchatReply(message, "[Whisper] ");
GetClient().Self.Chat(message, channelnum, ChatType.Whisper);
return BasicReply("ok");
}

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Services;
using System.Collections.Generic;
@ -444,5 +444,31 @@ namespace SecondBotEvents.Commands
master.DataStoreService.BotRecordReplyIM(groupUUID, message);
return BasicReply("Sending");
}
[About("fetchs a list of all pending group invitations")]
[ReturnHints("array of GroupInvite objects")]
[CmdTypeGet()]
public object GetPendingGroupInvites()
{
return BasicReply(JsonSerializer.Serialize(master.DataStoreService.GetPendingGroupInvites(), JsonOptions.UnsafeRelaxed));
}
[About("Responds to a group invitation")]
[ArgHints("group", "the group UUID to respond to", "UUID")]
[ArgHints("accept", "true to join, false to decline", "BOOL")]
[CmdTypeDo()]
public object GroupInviteRespond(string group, string accept)
{
if (UUID.TryParse(group, out UUID groupuuid) == false)
{
return Failure("Invaild group UUID", [group, accept]);
}
bool doAccept = accept.ToLower() == "true";
if (doAccept == true)
{
GetClient().Groups.RequestJoinGroup(groupuuid);
}
master.DataStoreService.RemovePendingGroupInvite(groupuuid);
return BasicReply("Done");
}
}
}

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Services;
using System;
@ -160,6 +160,19 @@ namespace SecondBotEvents.Commands
};
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
}
[About("Fetchs the current RLV restrictions applied to the bot")]
[ReturnHints("A JSON array of RLV rules")]
[CmdTypeGet()]
public object RLVStatus()
{
if (master.RLV == null)
{
return Failure("RLV service is not running");
}
var rules = master.RLV.Rules;
if (rules == null) return BasicReply("[]");
return BasicReply(JsonSerializer.Serialize(rules, JsonOptions.UnsafeRelaxed));
}
}
public class SculptysInfo

View file

@ -1,4 +1,4 @@
using SecondBotEvents.Services;
using SecondBotEvents.Services;
using System;
using System.Linq;
using System.Reflection;
@ -17,6 +17,12 @@ namespace SecondBotEvents
LogFormater.Info("Welcome to Secondbot [Events build] version: " + AssemblyInfo.GetGitHash());
EventsSecondBot worker = new(args);
AppDomain.CurrentDomain.ProcessExit += (s, e) => {
LogFormater.Warn("SIGTERM received, initiating graceful shutdown...");
worker.StopServices();
Thread.Sleep(2000);
};
while (worker.Exit() == false)
{
if (worker.Ready == true)

View file

@ -1,4 +1,4 @@
using OpenMetaverse;
using OpenMetaverse;
using OpenMetaverse.Assets;
using SecondBotEvents.Config;
using System;
@ -80,6 +80,13 @@ namespace SecondBotEvents.Services
AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer = null;
}
if (client != null && client.Network != null && client.Network.Connected)
{
LogFormater.Warn("Service stopping - initiating logout");
flagLogoutExpected();
client.Network.Logout();
Thread.Sleep(3000); // Wait for packets to send
}
ResetClient();
}

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Config;
using System;
@ -15,6 +15,16 @@ using Swan;
namespace SecondBotEvents.Services
{
public class GroupInvite
{
public UUID GroupID { get; set; }
public string GroupName { get; set; }
public UUID AgentID { get; set; }
public string AgentName { get; set; }
public UUID RoleID { get; set; }
public string Message { get; set; }
}
public class DataStoreService : BotServices
{
protected new DataStoreConfig myConfig;
@ -46,6 +56,7 @@ namespace SecondBotEvents.Services
protected Dictionary<string, long> KeyValueStoreLastUsed = [];
protected List<CommandHistory> commandHistories = [];
protected List<GroupInvite> PendingGroupInvites = [];
@ -469,11 +480,11 @@ namespace SecondBotEvents.Services
{
RecordChat(sessionid, "Bot", message);
}
public void BotRecordLocalchatReply(string message)
public void BotRecordLocalchatReply(string message, string typeTag = "")
{
lock (localChatHistory)
{
localChatHistory.Add("{BOT} " + GetClient().Self.Name + ": " + message);
localChatHistory.Add("{BOT} " + typeTag + GetClient().Self.Name + ": " + message);
if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{
return;
@ -599,6 +610,42 @@ namespace SecondBotEvents.Services
return KnownAvatar(name, UUID.Zero);
}
public void AddPendingGroupInvite(UUID groupID, UUID agentID, string agentName, string message)
{
lock (PendingGroupInvites)
{
// Avoid duplicates
if (PendingGroupInvites.Any(i => i.GroupID == groupID))
{
return;
}
PendingGroupInvites.Add(new GroupInvite
{
GroupID = groupID,
GroupName = "Unknown Group", // Will be updated on lookup
AgentID = agentID,
AgentName = agentName,
Message = message
});
}
}
public List<GroupInvite> GetPendingGroupInvites()
{
lock (PendingGroupInvites)
{
return [.. PendingGroupInvites];
}
}
public void RemovePendingGroupInvite(UUID groupID)
{
lock (PendingGroupInvites)
{
PendingGroupInvites.RemoveAll(i => i.GroupID == groupID);
}
}
protected int ObjectsAvatarsHash = 0;
/*
@ -1135,6 +1182,8 @@ namespace SecondBotEvents.Services
case ChatType.Whisper:
case ChatType.Normal:
case ChatType.Shout:
case ChatType.OwnerSay:
case ChatType.RegionSayTo:
{
if (hard_blocked_agents.Contains(e.FromName.ToLowerInvariant()) == true)
{
@ -1144,7 +1193,11 @@ namespace SecondBotEvents.Services
{
break;
}
string source = "{Av}";
if (string.IsNullOrWhiteSpace(e.Message))
{
break;
}
string source = "{Av}";
if (e.SourceType == ChatSourceType.Object)
{
source = "{Obj}";
@ -1153,6 +1206,13 @@ namespace SecondBotEvents.Services
{
source = "{Sys}";
}
string typeTag = "";
if (e.Type == ChatType.Shout) typeTag = "[Shout] ";
else if (e.Type == ChatType.Whisper) typeTag = "[Whisper] ";
else if (e.Type == ChatType.OwnerSay) typeTag = "[OwnerSay] ";
else if (e.Type == ChatType.RegionSayTo) typeTag = "[RegionSayTo] ";
lock (localChatHistory)
{
if (source == "{Av}")
@ -1160,7 +1220,7 @@ namespace SecondBotEvents.Services
AddAvatar(e.SourceID, e.FromName);
}
localChatHistory.Add(source+ " "+e.FromName + ": " + e.Message);
localChatHistory.Add(source + " " + typeTag + e.FromName + ": " + e.Message);
if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{
break;

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using OpenMetaverse;
using SecondBotEvents.Config;
using System;
@ -233,10 +233,13 @@ namespace SecondBotEvents.Services
Dictionary<string, string> reply = new()
{
{ "message", e.Message },
{ "fromuuid", e.AgentID.ToString() }
{ "fromuuid", e.AgentID.ToString() },
{ "groupuuid", e.GroupID.ToString() }
};
if (ProcessRequest(myConfig.GetAcceptGroupInvites(),myConfig.GetGroupInviteLevel(), e.FromName, "GroupInvite", e.AgentID, reply) == false)
{
// Not auto-accepted, record for dashboard
master.DataStoreService.AddPendingGroupInvite(e.GroupID, e.AgentID, e.FromName, e.Message);
return;
}
e.Accept = true;

View file

@ -1,4 +1,4 @@
using Discord;
using Discord;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using Org.BouncyCastle.Asn1.Ocsp;
@ -242,6 +242,7 @@ namespace SecondBotEvents.Services
EventsSecondBot instance = null;
readonly Regex rlv_regex = new(@"(?<behaviour>[^:=]+)(:(?<option>[^=]*))?=(?<param>\w+)", RegexOptions.Compiled);
readonly List<RLVRule> rules = [];
public List<RLVRule> Rules => rules;
System.Timers.Timer CleanupTimer;
void StartTimer()