feat: enhance bot commands, group handling, friend management, and avatar pick retrieval

This commit is contained in:
RebeccaNod1 2026-05-08 01:29:57 -06:00
parent ad6fb6a8d1
commit 5627b66229
7 changed files with 308 additions and 21 deletions

View file

@ -216,6 +216,27 @@ namespace SecondBotEvents.Commands
}
return BasicReply(reply.Value.AboutText.ToString());
}
[About("Requests the given avatars profile picks")]
[ReturnHints("JSON array of picks")]
[ReturnHintsFailure("Invaild avatar uuid")]
[ReturnHints("Requesting avatar details [Retry later]")]
[ArgHints("avatar", "Who are we getting the profile picks of", "AVATAR")]
[CmdTypeGet()]
public object GetAvatarPicks(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, Dictionary<UUID, string>> reply = master.DataStoreService.GetAvatarPicks(avataruuid);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]", [avatar]);
}
return BasicReply(JsonSerializer.Serialize(reply.Value, JsonOptions.UnsafeRelaxed));
}
[About("Requests the given avatars display name")]
[ReturnHints("display name if set or ?")]

View file

@ -12,6 +12,14 @@ namespace SecondBotEvents.Commands
{
return BasicReply("world");
}
[About("Returns the bot's Second Life name")]
[ReturnHints("Bot Name")]
[CmdTypeGet()]
public object GetBotName()
{
return BasicReply(GetClient().Self.Name);
}
}
public class CommandLibCall

View file

@ -1,4 +1,4 @@
using OpenMetaverse;
using OpenMetaverse;
using SecondBotEvents.Services;
using System.Text.Json;
using System.Collections.Generic;
@ -129,6 +129,56 @@ namespace SecondBotEvents.Commands
return BasicReply("Request sent", [avatar, state]);
}
[About("Gets the pending friend requests")]
[ReturnHints("array of FriendRequest")]
[CmdTypeGet()]
public object FriendsPendingRequests()
{
List<FriendRequest> requests = master.DataStoreService.GetPendingFriendRequests();
return BasicReply(JsonSerializer.Serialize(requests, JsonOptions.UnsafeRelaxed));
}
[About("Responds to a friend request")]
[ArgHints("avatar", "Who to respond to","AVATAR")]
[ArgHints("accept", "true: Accept, false: Decline","BOOL")]
[ReturnHints("accepted")]
[ReturnHints("declined")]
[ReturnHintsFailure("avatar lookup")]
[ReturnHintsFailure("no pending request")]
[ReturnHintsFailure("state invaild")]
[CmdTypeDo()]
public object FriendRequestRespond(string avatar, string accept)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("avatar lookup", [avatar, accept]);
}
if (bool.TryParse(accept, out bool status) == false)
{
return Failure("state invaild", [avatar, accept]);
}
List<FriendRequest> requests = master.DataStoreService.GetPendingFriendRequests();
FriendRequest request = requests.FirstOrDefault(r => r.AgentID == avataruuid);
if (request == null)
{
return Failure("no pending request", [avatar, accept]);
}
if (status == true)
{
GetClient().Friends.AcceptFriendship(avataruuid, request.SessionID);
master.DataStoreService.RemovePendingFriendRequest(avataruuid);
return BasicReply("accepted", [avatar, accept]);
}
else
{
GetClient().Friends.DeclineFriendship(avataruuid, request.SessionID);
master.DataStoreService.RemovePendingFriendRequest(avataruuid);
return BasicReply("declined", [avatar, accept]);
}
}
}
class FriendListEntry

View file

@ -27,6 +27,20 @@ namespace SecondBotEvents.Commands
return BasicReply(master.DataStoreService.IsGroupMember(groupuuid, avataruuid).ToString());
}
[About("Gets the rank of an avatar in a group (Guest, Member, Officer, Owner)")]
[ArgHints("group", "the group to check", "UUID")]
[ArgHints("avatar", "the avatar to check", "AVATAR")]
[CmdTypeGet()]
public object GetAvatarGroupRank(string group, string avatar)
{
if (UUID.TryParse(group, out UUID groupuuid) == false)
{
return Failure("Invaild group UUID", [group, avatar]);
}
ProcessAvatar(avatar);
return BasicReply(master.DataStoreService.GetGroupMemberRank(groupuuid, avataruuid));
}
[About("Gets membership of a group")]
[ReturnHints("list of UUIDS of group members")]
[ReturnHintsFailure("Updating")]

View file

@ -4,12 +4,38 @@ using SecondBotEvents.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace SecondBotEvents.Commands
{
[ClassInfo("What is going on with the bot")]
public class Info(EventsSecondBot setmaster) : CommandsAPI(setmaster)
{
[About("Requests a 6-digit login token to be sent via IM to the avatar")]
[ArgHints("avatar", "The avatar to send the token to", "AVATAR")]
[CmdTypeDo()]
public object RequestLoginToken(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero) return Failure("Unknown avatar - are you sure the name is correct? (Firstname Lastname)");
string token = master.DataStoreService.RequestLoginToken(avataruuid);
GetClient().Self.InstantMessage(avataruuid, $"[Bot Dashboard] Your login verification code is: {token} (Expires in 5 minutes)");
return BasicReply("Sent");
}
[About("Verifies a 6-digit login token")]
[ArgHints("avatar", "The avatar verifying", "AVATAR")]
[ArgHints("token", "The 6-digit token", "STRING")]
[CmdTypeGet()]
public object VerifyLoginToken(string avatar, string token)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero) return Failure("Unknown avatar");
bool ok = master.DataStoreService.VerifyLoginToken(avataruuid, token);
return BasicReply(ok.ToString());
}
[About("Fetchs the current region type the bot is in")]
[ReturnHints("Region type as string")]
[ReturnHintsFailure("Error not in a sim")]

View file

@ -25,6 +25,14 @@ namespace SecondBotEvents.Services
public string Message { get; set; }
}
public class FriendRequest
{
public UUID AgentID { get; set; }
public string AgentName { get; set; }
public UUID SessionID { get; set; }
public string Message { get; set; }
}
public class DataStoreService : BotServices
{
protected new DataStoreConfig myConfig;
@ -42,8 +50,9 @@ namespace SecondBotEvents.Services
protected Dictionary<UUID, string> groupsKey2Name = [];
protected Dictionary<UUID, List<UUID>> groupMembers = [];
protected Dictionary<UUID, Dictionary<UUID, GroupMember>> groupMembers = [];
protected Dictionary<UUID, Dictionary<UUID, GroupRole>> groupRoles = [];
protected Dictionary<UUID, KeyValuePair<string, long>> loginTokens = []; // Avatar -> {Token, Expiry}
protected List<string> localChatHistory = [];
@ -57,6 +66,7 @@ namespace SecondBotEvents.Services
protected List<CommandHistory> commandHistories = [];
protected List<GroupInvite> PendingGroupInvites = [];
protected List<FriendRequest> PendingFriendRequests = [];
@ -393,6 +403,59 @@ namespace SecondBotEvents.Services
}
}
public string GetGroupMemberRank(UUID group, UUID avatar)
{
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}");
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))
{
// 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.
}
return "Member";
}
}
public string RequestLoginToken(UUID avatar)
{
string token = _delayRandom.Next(100000, 999999).ToString();
long expiry = SecondbotHelpers.UnixTimeNow() + 300; // 5 mins
lock (loginTokens)
{
loginTokens[avatar] = new KeyValuePair<string, long>(token, expiry);
}
return token;
}
public bool VerifyLoginToken(UUID avatar, string token)
{
lock (loginTokens)
{
if (loginTokens.ContainsKey(avatar) == false) return false;
if (loginTokens[avatar].Value < SecondbotHelpers.UnixTimeNow())
{
loginTokens.Remove(avatar);
return false;
}
if (loginTokens[avatar].Key == token)
{
loginTokens.Remove(avatar);
return true;
}
return false;
}
}
public Dictionary<UUID, string> GetGroupRoles(UUID group)
{
Dictionary<UUID, string> reply = [];
@ -432,9 +495,9 @@ namespace SecondBotEvents.Services
{
if (groupMembers.ContainsKey(group) == false)
{
groupMembers.Add(group, []);
groupMembers.Add(group, new Dictionary<UUID, GroupMember>());
}
if (groupMembers[group].Contains(avatar) == false)
if (groupMembers[group].ContainsKey(avatar) == false)
{
reply = false;
AutoResetEvent fetchEvent = new(false);
@ -470,10 +533,10 @@ namespace SecondBotEvents.Services
{
if (groupMembers.ContainsKey(Group) == false)
{
groupMembers.Add(Group, []);
groupMembers.Add(Group, new Dictionary<UUID, GroupMember>());
}
return groupMembers[Group].Keys.ToList();
}
return groupMembers[Group];
}
public void BotRecordReplyIM(UUID sessionid,string message)
@ -646,6 +709,41 @@ namespace SecondBotEvents.Services
}
}
public void AddPendingFriendRequest(UUID agentID, string agentName, UUID sessionID, string message)
{
lock (PendingFriendRequests)
{
// Avoid duplicates
if (PendingFriendRequests.Any(r => r.AgentID == agentID))
{
return;
}
PendingFriendRequests.Add(new FriendRequest
{
AgentID = agentID,
AgentName = agentName,
SessionID = sessionID,
Message = message
});
}
}
public List<FriendRequest> GetPendingFriendRequests()
{
lock (PendingFriendRequests)
{
return [.. PendingFriendRequests];
}
}
public void RemovePendingFriendRequest(UUID agentID)
{
lock (PendingFriendRequests)
{
PendingFriendRequests.RemoveAll(r => r.AgentID == agentID);
}
}
protected int ObjectsAvatarsHash = 0;
/*
@ -967,14 +1065,18 @@ namespace SecondBotEvents.Services
{
lock (groupMembers)
{
if (groupMembers.ContainsKey(e.GroupID) == true)
if (groupMembers.ContainsKey(e.GroupID) == false)
{
groupMembers.Remove(e.GroupID);
groupMembers.Add(e.GroupID, new Dictionary<UUID, GroupMember>());
}
groupMembers.Add(e.GroupID, []);
else
{
groupMembers[e.GroupID].Clear();
}
foreach (KeyValuePair<UUID, GroupMember> a in e.Members)
{
groupMembers[e.GroupID].Add(a.Value.ID);
groupMembers[e.GroupID][a.Key] = a.Value;
}
}
}
@ -1053,6 +1155,30 @@ namespace SecondBotEvents.Services
}
Dictionary<UUID, Avatar.AvatarProperties> avprops = [];
Dictionary<UUID, Dictionary<UUID, string>> avpicks = [];
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID)
{
if (avpicks.ContainsKey(agentID) == false)
{
GetClient().Avatars.RequestAvatarPicks(agentID);
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
}
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avpicks[agentID]);
}
protected void AvatarPicksReply(object sender, AvatarPicksReplyEventArgs e)
{
lock (avpicks)
{
if (avpicks.ContainsKey(e.AvatarID) == false)
{
avpicks.Add(e.AvatarID, []);
}
avpicks[e.AvatarID] = e.Picks;
}
}
public KeyValuePair<bool,Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID)
{
if (avprops.ContainsKey(agentID) == false)
@ -1087,6 +1213,7 @@ namespace SecondBotEvents.Services
GetClient().Estate.EstateBansReply += EstateBans;
GetClient().Friends.FriendFoundReply += FindFriendReply;
GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply;
GetClient().Avatars.AvatarPicksReply += AvatarPicksReply;
GetClient().Groups.RequestCurrentGroups();
_ = GetClient().Self.RetrieveInstantMessages();
if(myConfig.GetPrefetchInventory() == true)
@ -1395,6 +1522,12 @@ namespace SecondBotEvents.Services
}
break;
}
case InstantMessageDialog.FriendshipOffered:
{
AddAvatar(e.IM.FromAgentID, e.IM.FromAgentName);
AddPendingFriendRequest(e.IM.FromAgentID, e.IM.FromAgentName, e.IM.IMSessionID, e.IM.Message);
break;
}
default:
{
break;

View file

@ -1,4 +1,4 @@
using EmbedIO;
using EmbedIO;
using EmbedIO.Actions;
using EmbedIO.Routing;
using EmbedIO.Utilities;
@ -173,6 +173,7 @@ namespace SecondBotEvents.Services
{
protected EventsSecondBot master = setmaster;
protected UUID avataruuid = UUID.Zero;
protected string avatarname = "";
protected Parcel targetparcel = null;
protected GridClient GetClient()
@ -204,20 +205,54 @@ namespace SecondBotEvents.Services
{
if (UUID.TryParse(avatar, out avataruuid) == true)
{
// sent a UUID not a name, thats fine
// try and load the name anyway
master.DataStoreService.GetAvatarName(avataruuid);
avatarname = master.DataStoreService.GetAvatarName(avataruuid);
return;
}
avataruuid = UUID.Zero;
string UUIDfetch = master.DataStoreService.GetAvatarUUID(avatar);
if (UUIDfetch != "lookup")
avatarname = avatar;
string UUIDfetch = master.DataStoreService.GetAvatarUUID(avatarname);
if (UUID.TryParse(UUIDfetch, out avataruuid) == false)
{
if(UUID.TryParse(UUIDfetch, out avataruuid) == false)
{
avataruuid = UUID.Zero;
}
avataruuid = UUID.Zero;
}
if (avataruuid == UUID.Zero)
{
avataruuid = ResolveAvatarUUID(avatarname);
}
}
protected UUID ResolveAvatarUUID(string avatar)
{
if (UUID.TryParse(avatar, out UUID result) == true) return result;
AutoResetEvent resolved = new(false);
UUID queryID = UUID.Random();
UUID resolvedUUID = UUID.Zero;
EventHandler<AvatarPickerReplyEventArgs> callback = (sender, e) => {
if (e.QueryID == queryID)
{
foreach (var kvp in e.Avatars)
{
if (kvp.Value.Equals(avatar, StringComparison.OrdinalIgnoreCase))
{
resolvedUUID = kvp.Key;
break;
}
}
if (resolvedUUID == UUID.Zero && e.Avatars.Count > 0)
{
foreach (var kvp in e.Avatars) { resolvedUUID = kvp.Key; break; }
}
resolved.Set();
}
};
GetClient().Avatars.AvatarPickerReply += callback;
GetClient().Avatars.RequestAvatarNameSearch(avatar, queryID);
resolved.WaitOne(5000);
GetClient().Avatars.AvatarPickerReply -= callback;
return resolvedUUID;
}
protected object BasicReply(string input)