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; 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); OnGroupInvitation(args);
if (args.Accept) if (args.Accept)
@ -2460,15 +2460,19 @@ namespace OpenMetaverse
/// <summary>The Simulator</summary> /// <summary>The Simulator</summary>
public Simulator Simulator { get; } public Simulator Simulator { get; }
/// <summary>The Group ID</summary>
public UUID GroupID { get; }
/// <summary>Set to true to accept invitation, false to decline</summary> /// <summary>Set to true to accept invitation, false to decline</summary>
public bool Accept { get; set; } 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; Simulator = simulator;
AgentID = agentID; AgentID = agentID;
FromName = agentName; FromName = agentName;
Message = message; Message = message;
GroupID = groupID;
} }
} }

View file

@ -78,25 +78,73 @@ namespace SecondBotEvents.Commands
return BasicReply("Error not in a sim"); return BasicReply("Error not in a sim");
} }
List<NearMeDetails> BetterNearMe = []; 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.ID == GetClient().Self.AgentID) continue;
if (av.ParentID != 0 && GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(av.ParentID, out Primitive parentObj))
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() NearMeDetails details = new()
{ {
id = av.ID.ToString(), id = av.ID.ToString(),
name = av.Name, name = av.Name,
x = (int)Math.Round(globalPos.X), x = (int)Math.Round(relX),
y = (int)Math.Round(globalPos.Y), y = (int)Math.Round(relY),
z = (int)Math.Round(globalPos.Z), 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); BetterNearMe.Add(details);
} }

View file

@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using SecondBotEvents.Services; using SecondBotEvents.Services;
@ -31,7 +31,8 @@ namespace SecondBotEvents.Commands
{ {
return Failure("Invaild channel", [channel, message]); 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); GetClient().Self.Chat(message, channelnum, ChatType.Normal);
return BasicReply("ok"); return BasicReply("ok");
} }
@ -52,7 +53,7 @@ namespace SecondBotEvents.Commands
{ {
return Failure("Invaild channel", [channel, message]); return Failure("Invaild channel", [channel, message]);
} }
master.DataStoreService.BotRecordLocalchatReply(message); master.DataStoreService.BotRecordLocalchatReply(message, "[Shout] ");
GetClient().Self.Chat(message, channelnum, ChatType.Shout); GetClient().Self.Chat(message, channelnum, ChatType.Shout);
return BasicReply("ok"); return BasicReply("ok");
} }
@ -73,7 +74,7 @@ namespace SecondBotEvents.Commands
{ {
return Failure("Invaild channel", [channel, message]); return Failure("Invaild channel", [channel, message]);
} }
master.DataStoreService.BotRecordLocalchatReply(message); master.DataStoreService.BotRecordLocalchatReply(message, "[Whisper] ");
GetClient().Self.Chat(message, channelnum, ChatType.Whisper); GetClient().Self.Chat(message, channelnum, ChatType.Whisper);
return BasicReply("ok"); return BasicReply("ok");
} }

View file

@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using SecondBotEvents.Services; using SecondBotEvents.Services;
using System.Collections.Generic; using System.Collections.Generic;
@ -444,5 +444,31 @@ namespace SecondBotEvents.Commands
master.DataStoreService.BotRecordReplyIM(groupUUID, message); master.DataStoreService.BotRecordReplyIM(groupUUID, message);
return BasicReply("Sending"); 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 OpenMetaverse;
using SecondBotEvents.Services; using SecondBotEvents.Services;
using System; using System;
@ -160,6 +160,19 @@ namespace SecondBotEvents.Commands
}; };
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed)); 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 public class SculptysInfo

View file

@ -1,4 +1,4 @@
using SecondBotEvents.Services; using SecondBotEvents.Services;
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -17,6 +17,12 @@ namespace SecondBotEvents
LogFormater.Info("Welcome to Secondbot [Events build] version: " + AssemblyInfo.GetGitHash()); LogFormater.Info("Welcome to Secondbot [Events build] version: " + AssemblyInfo.GetGitHash());
EventsSecondBot worker = new(args); 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) while (worker.Exit() == false)
{ {
if (worker.Ready == true) if (worker.Ready == true)

View file

@ -1,4 +1,4 @@
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.Assets; using OpenMetaverse.Assets;
using SecondBotEvents.Config; using SecondBotEvents.Config;
using System; using System;
@ -80,6 +80,13 @@ namespace SecondBotEvents.Services
AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer = null; 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(); ResetClient();
} }

View file

@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using SecondBotEvents.Config; using SecondBotEvents.Config;
using System; using System;
@ -15,6 +15,16 @@ using Swan;
namespace SecondBotEvents.Services 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 public class DataStoreService : BotServices
{ {
protected new DataStoreConfig myConfig; protected new DataStoreConfig myConfig;
@ -46,6 +56,7 @@ namespace SecondBotEvents.Services
protected Dictionary<string, long> KeyValueStoreLastUsed = []; protected Dictionary<string, long> KeyValueStoreLastUsed = [];
protected List<CommandHistory> commandHistories = []; protected List<CommandHistory> commandHistories = [];
protected List<GroupInvite> PendingGroupInvites = [];
@ -469,11 +480,11 @@ namespace SecondBotEvents.Services
{ {
RecordChat(sessionid, "Bot", message); RecordChat(sessionid, "Bot", message);
} }
public void BotRecordLocalchatReply(string message) public void BotRecordLocalchatReply(string message, string typeTag = "")
{ {
lock (localChatHistory) lock (localChatHistory)
{ {
localChatHistory.Add("{BOT} " + GetClient().Self.Name + ": " + message); localChatHistory.Add("{BOT} " + typeTag + GetClient().Self.Name + ": " + message);
if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit()) if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{ {
return; return;
@ -599,6 +610,42 @@ namespace SecondBotEvents.Services
return KnownAvatar(name, UUID.Zero); 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; protected int ObjectsAvatarsHash = 0;
/* /*
@ -1135,6 +1182,8 @@ namespace SecondBotEvents.Services
case ChatType.Whisper: case ChatType.Whisper:
case ChatType.Normal: case ChatType.Normal:
case ChatType.Shout: case ChatType.Shout:
case ChatType.OwnerSay:
case ChatType.RegionSayTo:
{ {
if (hard_blocked_agents.Contains(e.FromName.ToLowerInvariant()) == true) if (hard_blocked_agents.Contains(e.FromName.ToLowerInvariant()) == true)
{ {
@ -1144,7 +1193,11 @@ namespace SecondBotEvents.Services
{ {
break; break;
} }
string source = "{Av}"; if (string.IsNullOrWhiteSpace(e.Message))
{
break;
}
string source = "{Av}";
if (e.SourceType == ChatSourceType.Object) if (e.SourceType == ChatSourceType.Object)
{ {
source = "{Obj}"; source = "{Obj}";
@ -1153,6 +1206,13 @@ namespace SecondBotEvents.Services
{ {
source = "{Sys}"; 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) lock (localChatHistory)
{ {
if (source == "{Av}") if (source == "{Av}")
@ -1160,7 +1220,7 @@ namespace SecondBotEvents.Services
AddAvatar(e.SourceID, e.FromName); 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()) if (localChatHistory.Count <= myConfig.GetLocalChatHistoryLimit())
{ {
break; break;

View file

@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using SecondBotEvents.Config; using SecondBotEvents.Config;
using System; using System;
@ -233,10 +233,13 @@ namespace SecondBotEvents.Services
Dictionary<string, string> reply = new() Dictionary<string, string> reply = new()
{ {
{ "message", e.Message }, { "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) 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; return;
} }
e.Accept = true; e.Accept = true;

View file

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