diff --git a/SecondBotEvents/Commands/Avatars.cs b/SecondBotEvents/Commands/Avatars.cs index 515bc65..14eaed4 100644 --- a/SecondBotEvents/Commands/Avatars.cs +++ b/SecondBotEvents/Commands/Avatars.cs @@ -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> 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 ?")] diff --git a/SecondBotEvents/Commands/Core.cs b/SecondBotEvents/Commands/Core.cs index 6e35b03..d5b54c6 100644 --- a/SecondBotEvents/Commands/Core.cs +++ b/SecondBotEvents/Commands/Core.cs @@ -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 diff --git a/SecondBotEvents/Commands/Friends.cs b/SecondBotEvents/Commands/Friends.cs index 6ef4a34..fe159d9 100644 --- a/SecondBotEvents/Commands/Friends.cs +++ b/SecondBotEvents/Commands/Friends.cs @@ -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 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 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 diff --git a/SecondBotEvents/Commands/Group.cs b/SecondBotEvents/Commands/Group.cs index 85cec3f..6e51273 100644 --- a/SecondBotEvents/Commands/Group.cs +++ b/SecondBotEvents/Commands/Group.cs @@ -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")] diff --git a/SecondBotEvents/Commands/Info.cs b/SecondBotEvents/Commands/Info.cs index 56821dc..0ac1620 100644 --- a/SecondBotEvents/Commands/Info.cs +++ b/SecondBotEvents/Commands/Info.cs @@ -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")] diff --git a/SecondBotEvents/Services/DataStoreService.cs b/SecondBotEvents/Services/DataStoreService.cs index 71d8157..977a4be 100644 --- a/SecondBotEvents/Services/DataStoreService.cs +++ b/SecondBotEvents/Services/DataStoreService.cs @@ -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 groupsKey2Name = []; - protected Dictionary> groupMembers = []; + protected Dictionary> groupMembers = []; protected Dictionary> groupRoles = []; + protected Dictionary> loginTokens = []; // Avatar -> {Token, Expiry} protected List localChatHistory = []; @@ -57,6 +66,7 @@ namespace SecondBotEvents.Services protected List commandHistories = []; protected List PendingGroupInvites = []; + protected List 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(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 GetGroupRoles(UUID group) { Dictionary reply = []; @@ -432,9 +495,9 @@ namespace SecondBotEvents.Services { if (groupMembers.ContainsKey(group) == false) { - groupMembers.Add(group, []); + groupMembers.Add(group, new Dictionary()); } - 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()); } + 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 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()); } - groupMembers.Add(e.GroupID, []); + else + { + groupMembers[e.GroupID].Clear(); + } + foreach (KeyValuePair 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 avprops = []; + Dictionary> avpicks = []; + + public KeyValuePair> GetAvatarPicks(UUID agentID) + { + if (avpicks.ContainsKey(agentID) == false) + { + GetClient().Avatars.RequestAvatarPicks(agentID); + return new KeyValuePair>(false, []); + } + return new KeyValuePair>(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 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; diff --git a/SecondBotEvents/Services/HttpService.cs b/SecondBotEvents/Services/HttpService.cs index 60b291a..42a72c3 100644 --- a/SecondBotEvents/Services/HttpService.cs +++ b/SecondBotEvents/Services/HttpService.cs @@ -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 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)