feat: persist pending group invites and resolve names, restore custom avatar methods
Some checks are pending
autobuild / docker (push) Waiting to run

This commit is contained in:
RebeccaNod1 2026-06-13 04:30:13 -06:00
parent ad2aab419f
commit f9094f6fca
24 changed files with 1350 additions and 67 deletions

View file

@ -1,9 +1,9 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
WORKDIR /app WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src WORKDIR /src
COPY ["NuGet.Config", "."] COPY ["NuGet.Config", "."]
COPY ["SecondBotEvents/SecondBotEvents.csproj", "SecondBotEvents/"] COPY ["SecondBotEvents/SecondBotEvents.csproj", "SecondBotEvents/"]

View file

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>LibreMetaverse.StructuredData</AssemblyName> <AssemblyName>LibreMetaverse.StructuredData</AssemblyName>
<Description>LibreMetaverse structured data library</Description> <Description>LibreMetaverse structured data library</Description>
<PackageId>LibreMetaverse.StructuredData</PackageId> <PackageId>LibreMetaverse.StructuredData</PackageId>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<RootNamespace>LibreMetaverse.StructuredData</RootNamespace> <RootNamespace>LibreMetaverse.StructuredData</RootNamespace>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <GenerateAssemblyInfo>true</GenerateAssemblyInfo>

View file

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>LibreMetaverse.Types</AssemblyName> <AssemblyName>LibreMetaverse.Types</AssemblyName>
<PackageId>LibreMetaverse.Types</PackageId> <PackageId>LibreMetaverse.Types</PackageId>
<Description>LibreMetaverse type library</Description> <Description>LibreMetaverse type library</Description>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<RootNamespace>LibreMetaverse</RootNamespace> <RootNamespace>LibreMetaverse</RootNamespace>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <GenerateAssemblyInfo>true</GenerateAssemblyInfo>

View file

@ -375,11 +375,11 @@ namespace OpenMetaverse
public ulong TimeOfDay { get; private set; } public ulong TimeOfDay { get; private set; }
/// <summary>A dictionary of all the regions, indexed by region name</summary> /// <summary>A dictionary of all the regions, indexed by region name</summary>
internal Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>(); public Dictionary<string, GridRegion> Regions = new Dictionary<string, GridRegion>();
/// <summary>A dictionary of all the regions, indexed by region handle</summary> /// <summary>A dictionary of all the regions, indexed by region handle</summary>
internal Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>(); public Dictionary<ulong, GridRegion> RegionsByHandle = new Dictionary<ulong, GridRegion>();
/// <summary>A dictionary of regions by region handle</summary> /// <summary>A dictionary of regions by region handle</summary>
internal Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>(); public Dictionary<UUID, ulong> RegionsByUUID = new Dictionary<UUID, ulong>();
private readonly GridClient Client; private readonly GridClient Client;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>LibreMetaverse</AssemblyName> <AssemblyName>LibreMetaverse</AssemblyName>
<PackageId>LibreMetaverse</PackageId> <PackageId>LibreMetaverse</PackageId>
@ -12,7 +12,7 @@
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<NoWarn>$(NoWarn);CS0419;CS8981;CS1591;CS1574</NoWarn> <NoWarn>$(NoWarn);CS0419;CS8981;CS1591;CS1574</NoWarn>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<OutputPath>..\bin\</OutputPath> <OutputPath>..\bin\</OutputPath>
</PropertyGroup> </PropertyGroup>

View file

@ -1,4 +1,5 @@
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.Assets;
using SecondBotEvents.Services; using SecondBotEvents.Services;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -7,8 +8,90 @@ using System.Linq;
namespace SecondBotEvents.Commands namespace SecondBotEvents.Commands
{ {
[ClassInfo("Animations and Gestures")] [ClassInfo("Animations and Gestures")]
internal class AnimationCommands(EventsSecondBot setmaster) : CommandsAPI(setmaster) public class AnimationCommands(EventsSecondBot setmaster) : CommandsAPI(setmaster)
{ {
private static readonly Dictionary<UUID, string> gestureTriggerCache = new Dictionary<UUID, string>();
private static readonly Dictionary<UUID, string> gestureReplaceCache = new Dictionary<UUID, string>();
private static bool isPreCachingStarted = false;
private static bool isPreCachingActive = false;
private static readonly string cacheFilePath = "data/gesture_cache.json";
private static bool isCacheLoaded = false;
private static readonly object cacheLock = new object();
public class GestureCacheEntry
{
public string Trigger { get; set; }
public string ReplaceWith { get; set; }
}
private static void LoadGestureCache()
{
lock (cacheLock)
{
if (isCacheLoaded) return;
isCacheLoaded = true;
try
{
System.IO.Directory.CreateDirectory("data");
if (System.IO.File.Exists(cacheFilePath))
{
string json = System.IO.File.ReadAllText(cacheFilePath);
var loaded = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, GestureCacheEntry>>(json);
if (loaded != null)
{
lock (gestureTriggerCache)
{
foreach (var kvp in loaded)
{
if (UUID.TryParse(kvp.Key, out UUID uuid))
{
gestureTriggerCache[uuid] = kvp.Value.Trigger;
gestureReplaceCache[uuid] = kvp.Value.ReplaceWith;
}
}
}
}
}
}
catch (Exception ex)
{
LogFormater.Warn("Error loading gesture cache: " + ex.Message);
}
}
}
private static void SaveGestureCache()
{
lock (cacheLock)
{
try
{
System.IO.Directory.CreateDirectory("data");
var dump = new Dictionary<string, GestureCacheEntry>();
lock (gestureTriggerCache)
{
foreach (var kvp in gestureTriggerCache)
{
string replaceWith = "";
gestureReplaceCache.TryGetValue(kvp.Key, out replaceWith);
dump[kvp.Key.ToString()] = new GestureCacheEntry
{
Trigger = kvp.Value,
ReplaceWith = replaceWith ?? ""
};
}
}
string json = System.Text.Json.JsonSerializer.Serialize(dump);
System.IO.File.WriteAllText(cacheFilePath, json);
}
catch (Exception ex)
{
LogFormater.Warn("Error saving gesture cache: " + ex.Message);
}
}
}
[About("Toggles if animation requests from this avatar (used for remote poseballs) are accepted")] [About("Toggles if animation requests from this avatar (used for remote poseballs) are accepted")]
[ReturnHints("Granted perm animation")] [ReturnHints("Granted perm animation")]
[ReturnHints("Removed perm animation")] [ReturnHints("Removed perm animation")]
@ -41,8 +124,10 @@ namespace SecondBotEvents.Commands
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15)); InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
if (itm == null) if (itm == null)
{ {
return BasicReply("unable to get getsture from inventory", [gesture]); return BasicReply("unable to get gesture from inventory", [gesture]);
} }
LogFormater.Info($"Playing gesture: {itm.Name} ({itm.AssetUUID})");
GetClient().Self.ActivateGesture(itm.UUID, itm.AssetUUID);
GetClient().Self.PlayGesture(itm.AssetUUID); GetClient().Self.PlayGesture(itm.AssetUUID);
return BasicReply("Accepted", [gesture]); return BasicReply("Accepted", [gesture]);
} }
@ -61,6 +146,339 @@ namespace SecondBotEvents.Commands
return BasicReply("Accepted", [gesture]); return BasicReply("Accepted", [gesture]);
} }
[About("Plays a built-in animation by UUID or name")]
[ArgHints("animation", "UUID or name of the animation", "STRING")]
[CmdTypeDo()]
public object PlayAnimation(string animation)
{
UUID animID;
if (!UUID.TryParse(animation, out animID))
{
var field = typeof(Animations).GetField(animation.ToUpper(), System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (field != null)
{
animID = (UUID)field.GetValue(null);
}
else
{
return Failure("Invalid animation name or UUID");
}
}
GetClient().Self.AnimationStart(animID, true);
return BasicReply("Accepted", [animation]);
}
private string GetGestureTrigger(InventoryItem itm)
{
LoadGestureCache();
lock (gestureTriggerCache)
{
if (gestureTriggerCache.TryGetValue(itm.AssetUUID, out string cachedTrigger))
{
return cachedTrigger;
}
}
AssetGesture gesture = null;
System.Threading.AutoResetEvent gotAsset = new System.Threading.AutoResetEvent(false);
GetClient().Assets.RequestAsset(itm.AssetUUID, AssetType.Gesture, true,
delegate(AssetDownload transfer, Asset asset)
{
if (transfer.Success)
{
gesture = (AssetGesture)asset;
}
gotAsset.Set();
}
);
// Wait 500ms max (gestures are tiny and take < 50ms usually)
gotAsset.WaitOne(TimeSpan.FromMilliseconds(500), false);
if (gesture != null && gesture.Decode())
{
string trigger = gesture.Trigger.Trim().ToLowerInvariant();
string replaceWith = gesture.ReplaceWith ?? "";
lock (gestureTriggerCache)
{
gestureTriggerCache[itm.AssetUUID] = trigger;
gestureReplaceCache[itm.AssetUUID] = replaceWith;
}
if (!isPreCachingActive)
{
SaveGestureCache();
}
return trigger;
}
return null;
}
private void StartPreCachingGestures()
{
LoadGestureCache();
if (isPreCachingStarted) return;
isPreCachingStarted = true;
System.Threading.ThreadPool.QueueUserWorkItem(delegate
{
try
{
List<InventoryItem> allGestures = new List<InventoryItem>();
UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture);
if (gestureFolder != UUID.Zero)
{
List<InventoryBase> contents = GetClient().Inventory.FolderContents(gestureFolder, GetClient().Self.AgentID, false, true, InventorySortOrder.ByName, TimeSpan.FromSeconds(15));
if (contents != null)
{
foreach (InventoryBase item in contents)
{
if (item is InventoryItem invItem && invItem.InventoryType == InventoryType.Gesture)
{
allGestures.Add(invItem);
}
}
}
}
Inventory store = GetClient().Inventory.GetStore;
lock (store.Items)
{
foreach (var node in store.Items.Values)
{
if (node.Data is InventoryItem item && item.InventoryType == InventoryType.Gesture)
{
if (!allGestures.Any(g => g.UUID == item.UUID))
{
allGestures.Add(item);
}
}
}
}
isPreCachingActive = true;
foreach (var item in allGestures)
{
GetGestureTrigger(item);
}
}
catch (Exception ex)
{
LogFormater.Warn("Error pre-caching gestures: " + ex.Message);
}
finally
{
isPreCachingActive = false;
SaveGestureCache();
}
});
}
private InventoryItem FindGestureInInventory(string search)
{
StartPreCachingGestures();
search = search.Trim().ToLowerInvariant();
string searchWithSlash = search.StartsWith("/") ? search : "/" + search;
string searchWithoutSlash = search.StartsWith("/") ? search.Substring(1) : search;
List<InventoryItem> allGestures = new List<InventoryItem>();
// 1. Gather all gestures from default Gestures folder
UUID gestureFolder = GetClient().Inventory.FindFolderForType(AssetType.Gesture);
if (gestureFolder != UUID.Zero)
{
List<InventoryBase> contents = GetClient().Inventory.FolderContents(gestureFolder, GetClient().Self.AgentID, false, true, InventorySortOrder.ByName, TimeSpan.FromSeconds(15));
if (contents != null)
{
foreach (InventoryBase item in contents)
{
if (item is InventoryItem invItem && invItem.InventoryType == InventoryType.Gesture)
{
allGestures.Add(invItem);
}
}
}
}
// 2. Gather from general inventory store cache
Inventory store = GetClient().Inventory.GetStore;
lock (store.Items)
{
foreach (var node in store.Items.Values)
{
if (node.Data is InventoryItem item && item.InventoryType == InventoryType.Gesture)
{
if (!allGestures.Any(g => g.UUID == item.UUID))
{
allGestures.Add(item);
}
}
}
}
// 3. Step A: Search for exact name match first (super fast, no asset download)
foreach (var item in allGestures)
{
string itemName = item.Name.Trim().ToLowerInvariant();
if (itemName == searchWithSlash || itemName == searchWithoutSlash)
{
return item;
}
}
// 4. Step B: Search for trigger match (downloads/decodes asset, then checks cache)
InventoryItem bestMatch = null;
int bestScore = -9999;
string botFirstName = GetClient().Self.FirstName.ToLowerInvariant();
bool isFemaleBot = botFirstName.Contains("girl") || botFirstName.Contains("female") || botFirstName.Contains("lady") || botFirstName.Contains("miss") || botFirstName.Contains("woman") || botFirstName.Contains("queen") || botFirstName.Contains("princess") || botFirstName.Contains("she");
bool isMaleBot = botFirstName.Contains("boy") || botFirstName.Contains("male") || botFirstName.Contains("guy") || botFirstName.Contains("man") || botFirstName.Contains("king") || botFirstName.Contains("prince") || botFirstName.Contains("he");
foreach (var item in allGestures)
{
string trigger = GetGestureTrigger(item);
if (trigger != null)
{
string triggerWithSlash = trigger.StartsWith("/") ? trigger : "/" + trigger;
string triggerWithoutSlash = trigger.StartsWith("/") ? trigger.Substring(1) : trigger;
if (trigger == search || triggerWithSlash == search || triggerWithoutSlash == search ||
trigger == searchWithSlash || trigger == searchWithoutSlash)
{
int score = 0;
string itemName = item.Name.ToLowerInvariant();
if (isFemaleBot)
{
if (itemName.Contains("female") || itemName.Contains("girl") || itemName.Contains("woman") || itemName.Contains("lady") || itemName.Contains("she"))
score += 10;
if (itemName.Contains("male") || itemName.Contains("boy") || itemName.Contains("man") || itemName.Contains("guy") || itemName.Contains("he"))
score -= 10;
}
else if (isMaleBot)
{
if (itemName.Contains("male") || itemName.Contains("boy") || itemName.Contains("man") || itemName.Contains("guy") || itemName.Contains("he"))
score += 10;
if (itemName.Contains("female") || itemName.Contains("girl") || itemName.Contains("woman") || itemName.Contains("lady") || itemName.Contains("she"))
score -= 10;
}
if (score > bestScore)
{
bestScore = score;
bestMatch = item;
}
}
}
}
if (bestMatch != null)
{
return bestMatch;
}
return null;
}
[About("Plays a gesture by name from the inventory")]
[ArgHints("gesture", "name of the gesture to play", "STRING")]
[CmdTypeDo()]
public object PlayGestureByName(string gesture)
{
string search = gesture.Trim().ToLowerInvariant();
// First try to match the exact string (like "clap" or ":-D")
InventoryItem foundGesture = FindGestureInInventory(search);
// If no exact match, try splitting into words (natural speak matching, like "i am a sad :-(")
if (foundGesture == null)
{
string[] words = search.Split(new[] { ' ', ',', '.', '!', '?' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string word in words)
{
foundGesture = FindGestureInInventory(word);
if (foundGesture != null)
break;
}
}
if (foundGesture == null)
{
return Failure("Gesture not found in inventory");
}
GetClient().Self.ActivateGesture(foundGesture.UUID, foundGesture.AssetUUID);
GetClient().Self.PlayGesture(foundGesture.AssetUUID);
string replaceWith = "";
lock (gestureTriggerCache)
{
gestureReplaceCache.TryGetValue(foundGesture.AssetUUID, out replaceWith);
}
master.DataStoreService.AddCommandToHistory(true, "PlayGestureByName", [gesture]);
return new
{
command = "PlayGestureByName",
args = new[] { gesture },
reply = "Accepted",
status = true,
replace_with = replaceWith ?? "",
matched_trigger = GetGestureTrigger(foundGesture) ?? ""
};
}
[About("Activates a gesture in the bot's inventory")]
[ArgHints("gesture", "inventory uuid of the gesture", "UUID")]
[CmdTypeDo()]
public object ActivateGesture(string gesture)
{
if (UUID.TryParse(gesture, out UUID gestureUUID) == false) return Failure("Invaild UUID");
InventoryItem itm = GetClient().Inventory.FetchItem(gestureUUID, GetClient().Self.AgentID, TimeSpan.FromSeconds(15));
if (itm == null) return Failure("unable to get gesture from inventory");
GetClient().Self.ActivateGesture(itm.UUID, itm.AssetUUID);
return BasicReply("Ok");
}
[About("Deactivates a gesture in the bot's inventory")]
[ArgHints("gesture", "inventory uuid of the gesture", "UUID")]
[CmdTypeDo()]
public object DeactivateGesture(string gesture)
{
if (UUID.TryParse(gesture, out UUID gestureUUID) == false) return Failure("Invaild UUID");
GetClient().Self.DeactivateGesture(gestureUUID);
return BasicReply("Ok");
}
[About("Clears the saved gesture cache and runs pre-caching again to pick up new or changed gestures")]
[CmdTypeDo()]
public object RecacheGestures()
{
lock (cacheLock)
{
try
{
if (System.IO.File.Exists(cacheFilePath))
{
System.IO.File.Delete(cacheFilePath);
}
}
catch {}
lock (gestureTriggerCache)
{
gestureTriggerCache.Clear();
gestureReplaceCache.Clear();
}
isCacheLoaded = false;
isPreCachingStarted = false;
}
StartPreCachingGestures();
return BasicReply("Recaching started");
}
[About("Resets the animation stack for the bot")] [About("Resets the animation stack for the bot")]
[ReturnHints("Accepted - X stopped animations")] [ReturnHints("Accepted - X stopped animations")]
[CmdTypeDo()] [CmdTypeDo()]

View file

@ -193,6 +193,7 @@ namespace SecondBotEvents.Commands
Avatar.AvatarProperties props = reply.Value; Avatar.AvatarProperties props = reply.Value;
props.ProfileImage = textureUUID; props.ProfileImage = textureUUID;
GetClient().Self.UpdateProfile(props); GetClient().Self.UpdateProfile(props);
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
return BasicReply("ok"); return BasicReply("ok");
} }
@ -230,7 +231,7 @@ namespace SecondBotEvents.Commands
{ {
return Failure("Invaild avatar uuid", [avatar]); return Failure("Invaild avatar uuid", [avatar]);
} }
KeyValuePair<bool, Dictionary<UUID, string>> reply = master.DataStoreService.GetAvatarPicks(avataruuid); KeyValuePair<bool, Dictionary<UUID, ProfilePick>> reply = master.DataStoreService.GetAvatarPicksDetailed(avataruuid);
if (reply.Key == false) if (reply.Key == false)
{ {
return Failure("Requesting avatar details [Retry later]", [avatar]); return Failure("Requesting avatar details [Retry later]", [avatar]);
@ -259,7 +260,7 @@ namespace SecondBotEvents.Commands
[ReturnHints("Requesting avatar details [Retry later]")] [ReturnHints("Requesting avatar details [Retry later]")]
[ReturnHintsFailure("about text to short")] [ReturnHintsFailure("about text to short")]
[ReturnHintsFailure("about text to long")] [ReturnHintsFailure("about text to long")]
[ArgHints("abouttext", "text to use in the about me area, length 10 to 300", "Text", "Hello world!")] [ArgHints("abouttext", "text to use in the about me area, length 10 to 510 bytes", "Text", "Hello world!")]
[CmdTypeSet()] [CmdTypeSet()]
public object SetProfileAbout(string abouttext) public object SetProfileAbout(string abouttext)
{ {
@ -267,7 +268,7 @@ namespace SecondBotEvents.Commands
{ {
return Failure("About text to short", [abouttext]); return Failure("About text to short", [abouttext]);
} }
else if (abouttext.Length > 300) else if (System.Text.Encoding.UTF8.GetByteCount(abouttext) > 510)
{ {
return Failure("About text to long", [abouttext]); return Failure("About text to long", [abouttext]);
} }
@ -279,6 +280,7 @@ namespace SecondBotEvents.Commands
Avatar.AvatarProperties props = reply.Value; Avatar.AvatarProperties props = reply.Value;
props.AboutText = abouttext; props.AboutText = abouttext;
GetClient().Self.UpdateProfile(props); GetClient().Self.UpdateProfile(props);
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
return BasicReply("ok"); return BasicReply("ok");
} }
@ -335,5 +337,187 @@ namespace SecondBotEvents.Commands
} }
return BasicReply(master.DataStoreService.GetAvatarUUID(name)); return BasicReply(master.DataStoreService.GetAvatarUUID(name));
} }
[About("Sets the bot display name")]
[CmdTypeSet()]
public object SetDisplayName(string newname)
{
if (string.IsNullOrWhiteSpace(newname))
{
return Failure("Display name cannot be empty");
}
GetClient().Self.SetDisplayName(GetClient().Self.Name, newname);
return BasicReply("ok");
}
[About("Sets the bot first life profile text and image")]
[CmdTypeSet()]
public object SetFirstLife(string text, string image)
{
if (UUID.TryParse(image, out UUID imageUUID) == false)
{
return Failure("Invalid image uuid");
}
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(GetClient().Self.AgentID);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]");
}
Avatar.AvatarProperties props = reply.Value;
props.FirstLifeText = text;
props.FirstLifeImage = imageUUID;
GetClient().Self.UpdateProfile(props);
master.DataStoreService.UpdateAvatarProperties(GetClient().Self.AgentID, props);
return BasicReply("ok");
}
[About("Creates or updates a profile pick")]
[CmdTypeSet()]
public object SetAvatarPick(string id, string name, string desc, string snapshot, string sim, string pos)
{
if (UUID.TryParse(id, out UUID pickUUID) == false)
{
return Failure("Invalid pick uuid");
}
if (UUID.TryParse(snapshot, out UUID snapshotUUID) == false)
{
snapshotUUID = UUID.Zero;
}
Vector3 position = Vector3.Zero;
if (Vector3.TryParse(pos, out Vector3 parsedPos))
{
position = parsedPos;
}
Vector3d globalPos = GetClient().Self.GlobalPosition;
if (!string.IsNullOrWhiteSpace(sim) && sim == GetClient().Network.CurrentSim.Name)
{
Utils.LongToUInts(GetClient().Network.CurrentSim.Handle, out uint x, out uint y);
globalPos = new Vector3d(x + position.X, y + position.Y, position.Z);
}
GetClient().Self.PickInfoUpdate(pickUUID, false, UUID.Zero, name, globalPos, snapshotUUID, desc);
master.DataStoreService.ClearAvatarPicksCache(GetClient().Self.AgentID);
return BasicReply("ok");
}
[About("Deletes a profile pick")]
[CmdTypeSet()]
public object DeleteAvatarPick(string id)
{
if (UUID.TryParse(id, out UUID pickUUID) == false)
{
return Failure("Invalid pick uuid");
}
GetClient().Self.PickDelete(pickUUID);
master.DataStoreService.ClearAvatarPicksCache(GetClient().Self.AgentID);
return BasicReply("ok");
}
[About("Creates or updates a classified ad")]
[CmdTypeSet()]
public object SetAvatarClassified(string id, string name, string desc, string snapshot, string category, string price, string autorenew)
{
if (UUID.TryParse(id, out UUID classifiedUUID) == false)
{
return Failure("Invalid classified uuid");
}
if (UUID.TryParse(snapshot, out UUID snapshotUUID) == false)
{
snapshotUUID = UUID.Zero;
}
int adPrice = 50;
int.TryParse(price, out adPrice);
bool renew = autorenew.ToLower() == "true";
DirectoryManager.ClassifiedCategories cat = DirectoryManager.ClassifiedCategories.Shopping;
if (Enum.TryParse(category, out DirectoryManager.ClassifiedCategories parsedCat))
{
cat = parsedCat;
}
GetClient().Self.UpdateClassifiedInfo(classifiedUUID, cat, snapshotUUID, adPrice, name, desc, renew);
master.DataStoreService.ClearAvatarClassifiedsCache(GetClient().Self.AgentID);
return BasicReply("ok");
}
[About("Deletes a classified ad")]
[CmdTypeSet()]
public object DeleteAvatarClassified(string id)
{
if (UUID.TryParse(id, out UUID classifiedUUID) == false)
{
return Failure("Invalid classified uuid");
}
GetClient().Self.DeleteClassified(classifiedUUID);
master.DataStoreService.ClearAvatarClassifiedsCache(GetClient().Self.AgentID);
return BasicReply("ok");
}
[About("Requests the given avatars profile classifieds")]
[CmdTypeGet()]
public object GetAvatarClassifieds(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>> reply = master.DataStoreService.GetAvatarClassifiedsDetailed(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 first life profile text")]
[CmdTypeGet()]
public object GetFirstLifeText(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(avataruuid);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]", [avatar]);
}
return BasicReply(reply.Value.FirstLifeText.ToString());
}
[About("Requests the given avatars first life profile image")]
[CmdTypeGet()]
public object GetFirstLifeImage(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, Avatar.AvatarProperties> reply = master.DataStoreService.GetAvatarAvatarProperties(avataruuid);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]", [avatar]);
}
return BasicReply(reply.Value.FirstLifeImage.ToString());
}
[About("Requests the given avatars profile groups")]
[CmdTypeGet()]
public object GetAvatarGroups(string avatar)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("Invaild avatar uuid", [avatar]);
}
KeyValuePair<bool, List<AvatarGroup>> reply = master.DataStoreService.GetAvatarGroups(avataruuid);
if (reply.Key == false)
{
return Failure("Requesting avatar details [Retry later]", [avatar]);
}
return BasicReply(JsonSerializer.Serialize(reply.Value, JsonOptions.UnsafeRelaxed));
}
} }
} }

View file

@ -20,6 +20,14 @@ namespace SecondBotEvents.Commands
{ {
return BasicReply(GetClient().Self.Name); return BasicReply(GetClient().Self.Name);
} }
[About("Returns the bot's UUID")]
[ReturnHints("Bot UUID")]
[CmdTypeGet()]
public object GetBotUUID()
{
return BasicReply(GetClient().Self.AgentID.ToString());
}
} }
public class CommandLibCall public class CommandLibCall

View file

@ -44,21 +44,48 @@ namespace SecondBotEvents.Commands
{ {
Dictionary<UUID, FriendInfo> FriendListCopy = GetClient().Friends.FriendList.ToDictionary(k => k.Key, v => v.Value); Dictionary<UUID, FriendInfo> FriendListCopy = GetClient().Friends.FriendList.ToDictionary(k => k.Key, v => v.Value);
List< FriendListEntry > CleanedFriendsList = []; List< FriendListEntry > CleanedFriendsList = [];
int index = 0;
foreach (FriendInfo A in FriendListCopy.Values) foreach (FriendInfo A in FriendListCopy.Values)
{ {
FriendListEntry entry = new() FriendListEntry entry = new()
{ {
id = A.UUID.ToString(), id = A.UUID.ToString(),
name = A.Name, name = A.Name,
online = A.IsOnline online = A.IsOnline,
canSeeMeOnline = A.CanSeeMeOnline,
canSeeMeOnMap = A.CanSeeMeOnMap,
canModifyMyObjects = A.CanModifyMyObjects,
canSeeThemOnline = A.CanSeeThemOnline,
canSeeThemOnMap = A.CanSeeThemOnMap,
canModifyTheirObjects = A.CanModifyTheirObjects
}; };
CleanedFriendsList.Add(entry); CleanedFriendsList.Add(entry);
index++;
} }
return BasicReply(JsonSerializer.Serialize(CleanedFriendsList, JsonOptions.UnsafeRelaxed)); return BasicReply(JsonSerializer.Serialize(CleanedFriendsList, JsonOptions.UnsafeRelaxed));
} }
[About("Updates specific rights granted to a friend")]
[ArgHints("avatar", "The avatar to change rights for","AVATAR")]
[ArgHints("rights", "The exact integer mask of FriendRights to grant","INT")]
[CmdTypeSet()]
public object FriendSetRights(string avatar, string rights)
{
ProcessAvatar(avatar);
if (avataruuid == UUID.Zero)
{
return Failure("avatar lookup", [avatar, rights]);
}
if (int.TryParse(rights, out int mask) == false)
{
return Failure("invalid rights mask", [avatar, rights]);
}
if (GetClient().Friends.FriendList.ContainsKey(avataruuid) == false)
{
return Failure("Not A friend", [avatar, rights]);
}
GetClient().Friends.GrantRights(avataruuid, (FriendRights)mask);
return BasicReply("rights_updated", [avatar, rights]);
}
[About("Updates the friend perms for avatar avatar to State \n if true grants (Online/Map/Modify) perms")] [About("Updates the friend perms for avatar avatar to State \n if true grants (Online/Map/Modify) perms")]
[ArgHints("avatar", "Who we are talking about","AVATAR")] [ArgHints("avatar", "Who we are talking about","AVATAR")]
[ArgHints("state", "true: Grant perms, false: Remove perms","BOOL")] [ArgHints("state", "true: Grant perms, false: Remove perms","BOOL")]
@ -186,5 +213,11 @@ namespace SecondBotEvents.Commands
public string name; public string name;
public string id; public string id;
public bool online = false; public bool online = false;
public bool canSeeMeOnline = false;
public bool canSeeMeOnMap = false;
public bool canModifyMyObjects = false;
public bool canSeeThemOnline = false;
public bool canSeeThemOnMap = false;
public bool canModifyTheirObjects = false;
} }
} }

View file

@ -2,6 +2,7 @@ using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using SecondBotEvents.Services; using SecondBotEvents.Services;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace SecondBotEvents.Commands namespace SecondBotEvents.Commands
{ {
@ -354,24 +355,63 @@ namespace SecondBotEvents.Commands
return BasicReply("Working on it"); return BasicReply("Working on it");
} }
[About("Requests the roles for the selected group")] [About("Returns a list of roles for a group")]
[ReturnHints("GroupRoleDetails object")] [ReturnHints("dictionary uuid:name")]
[ReturnHintsFailure("Group is not currently known")]
[ReturnHintsFailure("Invaild group UUID")] [ReturnHintsFailure("Invaild group UUID")]
[ReturnHintsFailure("Updating")] [ReturnHintsFailure("Updating")]
[ArgHints("group", "the group to get roles from", "UUID")] [ArgHints("group", "the group to get roles from", "UUID")]
[ArgHints("botOnly", "only return roles the bot is in", "bool")]
[ArgHints("force", "force refresh from server", "bool")]
[CmdTypeGet()] [CmdTypeGet()]
public object GetGroupRoles(string group) public object GetGroupRoles(string group, string botOnly = "false", string force = "false")
{ {
if (UUID.TryParse(group, out UUID groupuuid) == false) if (UUID.TryParse(group, out UUID groupuuid) == false)
{ {
return Failure("Invaild group UUID", [group]); return Failure("Invaild group UUID", [group]);
} }
if (GetClient().Groups.GroupName2KeyCache.ContainsKey(groupuuid) == false)
bool doForce = force.ToLower() == "true";
Dictionary<UUID, string> allRoles = master.DataStoreService.GetGroupRoles(groupuuid, doForce);
if (botOnly.ToLower() == "true")
{ {
return Failure("Unknown group", [group]); Dictionary<UUID, string> botRoles = [];
AutoResetEvent fetchEvent = new(false);
void callback(object sender, GroupRolesMembersReplyEventArgs e)
{
if (e.GroupID == groupuuid)
{
foreach (KeyValuePair<UUID, UUID> pair in e.RolesMembers)
{
if (pair.Value == GetClient().Self.AgentID)
{
if (allRoles.ContainsKey(pair.Key))
{
botRoles[pair.Key] = allRoles[pair.Key];
} }
return BasicReply(JsonSerializer.Serialize(master.DataStoreService.GetGroupRoles(groupuuid), JsonOptions.UnsafeRelaxed)); }
}
fetchEvent.Set();
}
}
GetClient().Groups.GroupRoleMembersReply += callback;
GetClient().Groups.RequestGroupRolesMembers(groupuuid);
fetchEvent.WaitOne(2000, false);
GetClient().Groups.GroupRoleMembersReply -= callback;
// Ensure 'Everyone' role is included if we missed it
foreach(var role in allRoles)
{
if (role.Value.ToLower() == "everyone" || role.Key == UUID.Zero)
{
if (!botRoles.ContainsKey(role.Key)) botRoles[role.Key] = role.Value;
}
}
return BasicReply(JsonSerializer.Serialize(botRoles, JsonOptions.UnsafeRelaxed));
}
return BasicReply(JsonSerializer.Serialize(allRoles, JsonOptions.UnsafeRelaxed));
} }
[About("fetchs a list of all groups with unread messages")] [About("fetchs a list of all groups with unread messages")]
@ -483,5 +523,12 @@ namespace SecondBotEvents.Commands
master.DataStoreService.RemovePendingGroupInvite(groupuuid); master.DataStoreService.RemovePendingGroupInvite(groupuuid);
return BasicReply("Done"); return BasicReply("Done");
} }
[About("Gets the currently active group and title")]
[CmdTypeGet()]
public object GetGroupStatus()
{
return BasicReply(GetClient().Self.ActiveGroup.ToString(), [master.DataStoreService.GetActiveGroupTitle(), master.DataStoreService.GetGroupName(GetClient().Self.ActiveGroup)]);
}
} }
} }

View file

@ -179,11 +179,22 @@ namespace SecondBotEvents.Commands
{ {
return Failure("Error not in a sim"); return Failure("Error not in a sim");
} }
Dictionary<string, int> pos = new() uint sittingOn = GetClient().Self.SittingOn;
string sittingOnUUID = "00000000-0000-0000-0000-000000000000";
if (sittingOn != 0)
{
if (GetClient().Network.CurrentSim.ObjectsPrimitives.TryGetValue(sittingOn, out Primitive prim))
{
sittingOnUUID = prim.ID.ToString();
}
}
Dictionary<string, object> pos = new()
{ {
{ "x", (int)Math.Round(GetClient().Self.SimPosition.X) }, { "x", (int)Math.Round(GetClient().Self.SimPosition.X) },
{ "y", (int)Math.Round(GetClient().Self.SimPosition.Y) }, { "y", (int)Math.Round(GetClient().Self.SimPosition.Y) },
{ "z", (int)Math.Round(GetClient().Self.SimPosition.Z) } { "z", (int)Math.Round(GetClient().Self.SimPosition.Z) },
{ "sitting_on", sittingOnUUID }
}; };
return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed)); return BasicReply(JsonSerializer.Serialize(pos, JsonOptions.UnsafeRelaxed));
} }

View file

@ -1,4 +1,4 @@
using System.Text.Json; using System.Text.Json;
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14; using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Services; using SecondBotEvents.Services;
@ -86,6 +86,21 @@ namespace SecondBotEvents.Commands
return BasicReply(test.ToString()); return BasicReply(test.ToString());
} }
[About("Gets the current parcel's snapshot UUID")]
[ReturnHints("snapshot uuid")]
[ReturnHintsFailure("Error not in a sim")]
[ReturnHintsFailure("Parcel data not ready")]
[CmdTypeGet()]
public object GetCurrentParcelSnapshot()
{
KeyValuePair<bool, string> tests = SetupCurrentParcel();
if (tests.Key == false)
{
return Failure(tests.Value);
}
return BasicReply(targetparcel.SnapshotID.ToString());
}
[About("Requests the bot update its list of parcels ready for other commands")] [About("Requests the bot update its list of parcels ready for other commands")]
[ReturnHints("ok")] [ReturnHints("ok")]
[CmdTypeDo()] [CmdTypeDo()]

View file

@ -16,6 +16,14 @@ namespace SecondBotEvents.Config
settings.Add("DefaultHoverHeight"); settings.Add("DefaultHoverHeight");
settings.Add("MAC"); settings.Add("MAC");
settings.Add("Id0"); settings.Add("Id0");
settings.Add("StartLocation");
settings.Add("ReconnectDelaySecs");
settings.Add("MaxReconnectDelaySecs");
}
public string GetStartLocation()
{
return ReadSettingAsString("StartLocation", "last");
} }
public double GetDefaultHoverHeight() public double GetDefaultHoverHeight()
@ -67,5 +75,23 @@ namespace SecondBotEvents.Config
{ {
return ReadSettingAsString("Id0", ""); return ReadSettingAsString("Id0", "");
} }
/// <summary>
/// Base reconnect delay in seconds. Doubles on each consecutive failure (exponential backoff).
/// Default: 120 seconds.
/// </summary>
public int GetReconnectDelaySecs()
{
return ReadSettingAsInt("ReconnectDelaySecs", 120);
}
/// <summary>
/// Maximum reconnect delay cap in seconds for exponential backoff.
/// Default: 600 seconds (10 minutes).
/// </summary>
public int GetMaxReconnectDelaySecs()
{
return ReadSettingAsInt("MaxReconnectDelaySecs", 600);
}
} }
} }

View file

@ -0,0 +1,20 @@
using System;
namespace SecondBotEvents.Config
{
/// <summary>
/// Configuration for bot reconnection jitter timings.
/// </summary>
public class BotRejoinConfig
{
/// <summary>
/// Minimum jitter (in milliseconds) before a bot attempts to reconnect after a restart.
/// </summary>
public int JitterMinMs { get; set; } = 5000; // 5 seconds
/// <summary>
/// Maximum jitter (in milliseconds) before a bot attempts to reconnect after a restart.
/// </summary>
public int JitterMaxMs { get; set; } = 30000; // 30 seconds
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -10,6 +10,7 @@ namespace SecondBotEvents.Config
{ {
filename = "http"; filename = "http";
settings.Add("Enabled"); settings.Add("Enabled");
settings.Add("Port");
settings.Add("HideStatusOutput"); settings.Add("HideStatusOutput");
settings.Add("DisableCommandValidation"); settings.Add("DisableCommandValidation");
} }
@ -23,6 +24,11 @@ namespace SecondBotEvents.Config
{ {
return ReadSettingAsBool("DisableCommandValidation", false); return ReadSettingAsBool("DisableCommandValidation", false);
} }
public int GetPort()
{
return ReadSettingAsInt("Port", 80);
}
} }
} }

View file

@ -11,6 +11,16 @@ namespace SecondBotEvents
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
// Force initialize OpenMetaverse logger early to avoid log4net repository registration errors
try
{
var _ = OpenMetaverse.Logger.LogInstance;
}
catch (Exception ex)
{
Console.WriteLine($"[Startup] Warning: Failed to initialize OpenMetaverse logger: {ex.Message}");
}
#if (DEBUG) #if (DEBUG)
new WikiMake(); new WikiMake();
#endif #endif

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish"> <Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<StartupObject>SecondBotEvents.Startup</StartupObject> <StartupObject>SecondBotEvents.Startup</StartupObject>
<MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand> <MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand>
<MSBuildGitHashValidate>False</MSBuildGitHashValidate> <MSBuildGitHashValidate>False</MSBuildGitHashValidate>

View file

@ -12,8 +12,12 @@ namespace SecondBotEvents.Services
{ {
public GridClient client = null; public GridClient client = null;
public BasicConfig basicCfg = null; public BasicConfig basicCfg = null;
private BotRejoinConfig rejoinCfg;
protected bool LogoutExpected = false; protected bool LogoutExpected = false;
protected int consecutiveLoginFailures = 0;
protected DateTime lastConnectTime = DateTime.MinValue;
protected bool isRegionRestartScheduled = false;
public void flagLogoutExpected() public void flagLogoutExpected()
{ {
LogoutExpected = true; LogoutExpected = true;
@ -40,14 +44,19 @@ namespace SecondBotEvents.Services
public BotClientService(EventsSecondBot setMaster) : base(setMaster) public BotClientService(EventsSecondBot setMaster) : base(setMaster)
{ {
basicCfg = new BasicConfig(master.fromEnv, master.fromFolder); basicCfg = new BasicConfig(master.fromEnv, master.fromFolder);
rejoinCfg = new BotRejoinConfig();
} }
protected void RestartTimer(object o, ElapsedEventArgs e) protected void RestartTimer(object o, ElapsedEventArgs e)
{ {
AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer?.Stop();
AutoRestartLoginTimer = null; AutoRestartLoginTimer = null;
LogFormater.Info("Recovery underway - waiting 4 secs then restarting bot"); // Ensure logger is initialized early to avoid "logger not registered" errors
Thread.Sleep(4000); var _ = OpenMetaverse.Logger.LogInstance;
// Apply jitter before restart
int jitter = new Random().Next(rejoinCfg.JitterMinMs, rejoinCfg.JitterMaxMs);
LogFormater.Info($"Recovery sequence: Waiting {jitter}ms before restart (jitter applied)...");
Thread.Sleep(jitter);
Restart(); Restart();
} }
@ -90,9 +99,55 @@ namespace SecondBotEvents.Services
ResetClient(); ResetClient();
} }
protected void HandleUnexpectedDisconnect(string reason)
{
if (getLogoutExpected()) return;
int baseDelay = basicCfg.GetReconnectDelaySecs();
int maxDelay = basicCfg.GetMaxReconnectDelaySecs();
if (lastConnectTime != DateTime.MinValue && (DateTime.UtcNow - lastConnectTime).TotalSeconds > 60)
{
consecutiveLoginFailures = 0;
}
lastConnectTime = DateTime.MinValue;
int delaySecs = (int)Math.Min(baseDelay * Math.Pow(2, consecutiveLoginFailures), maxDelay);
if (isRegionRestartScheduled)
{
// Force a minimum 15 minutes (900 seconds) delay if a region restart is scheduled
delaySecs = Math.Max(delaySecs, 900);
}
consecutiveLoginFailures++;
LogFormater.Warn($"{reason} - retrying in {delaySecs}s (attempt {consecutiveLoginFailures})...");
StartRecoveryTimer(delaySecs * 1000);
}
protected void BotAlertMessage(object o, AlertMessageEventArgs e)
{
LogFormater.Info("Client service ~ Alert message: " + e.Message);
if (e.Message != null && e.Message.IndexOf("restart", StringComparison.OrdinalIgnoreCase) >= 0)
{
LogFormater.Warn("Detected upcoming region restart. Initiating graceful logout to prevent limbo state.");
isRegionRestartScheduled = true;
flagLogoutExpected();
if (client != null && client.Network != null)
{
client.Network.Logout();
}
// Directly start recovery timer with 15 minutes delay
StartRecoveryTimer(900 * 1000);
}
}
protected void BotLoggedOut(object o, LoggedOutEventArgs e) protected void BotLoggedOut(object o, LoggedOutEventArgs e)
{ {
LogFormater.Info("Client service ~ Logged out"); LogFormater.Info("Client service ~ Logged out");
if (!getLogoutExpected())
{
HandleUnexpectedDisconnect("Unexpected logout");
}
master.TriggerBotClientEvent(true, !getLogoutExpected()); master.TriggerBotClientEvent(true, !getLogoutExpected());
} }
@ -103,6 +158,8 @@ namespace SecondBotEvents.Services
AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer = null; AutoRestartLoginTimer = null;
} }
isRegionRestartScheduled = false;
lastConnectTime = DateTime.UtcNow;
goodticks = 0; goodticks = 0;
LogFormater.Info("Client service ~ Connected to sim: " + e.Simulator.Name); LogFormater.Info("Client service ~ Connected to sim: " + e.Simulator.Name);
} }
@ -111,20 +168,49 @@ namespace SecondBotEvents.Services
{ {
goodticks = 0; goodticks = 0;
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name); LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
if (!getLogoutExpected())
{
HandleUnexpectedDisconnect("Unexpected sim disconnect");
}
} }
protected void BotDisconnected(object o, DisconnectedEventArgs e) protected void BotDisconnected(object o, DisconnectedEventArgs e)
{ {
LogFormater.Info("Client service ~ Network disconnected: " + e.Message); LogFormater.Info("Client service ~ Network disconnected: " + e.Message);
if (!getLogoutExpected())
{
HandleUnexpectedDisconnect("Unexpected network disconnect: " + e.Message);
}
}
protected void StartRecoveryTimer(int intervalMs = 5000)
{
if (AutoRestartLoginTimer == null)
{
AutoRestartLoginTimer = new Timer
{
Interval = intervalMs,
AutoReset = false
};
AutoRestartLoginTimer.Elapsed += RestartTimer;
}
else
{
AutoRestartLoginTimer.Interval = intervalMs;
}
AutoRestartLoginTimer.Stop();
AutoRestartLoginTimer.Start();
} }
protected void BotLoginStatus(object o, LoginProgressEventArgs e) protected void BotLoginStatus(object o, LoginProgressEventArgs e)
{ {
if(e.FailReason != "") if (e.Status == LoginStatus.Failed || !string.IsNullOrEmpty(e.FailReason))
{ {
LogFormater.Info("Client service ~ {FAILED} Login status: " + e.FailReason.ToString()); string reason = string.IsNullOrEmpty(e.FailReason) ? e.Message : e.FailReason;
LogFormater.Info("Client service ~ {FAILED} Login status: " + reason);
client.Network.Logout(); client.Network.Logout();
ResetClient(); ResetClient();
HandleUnexpectedDisconnect("Login failed: " + reason);
return; return;
} }
LogFormater.Info("Client service ~ Login status: " + e.Status.ToString()); LogFormater.Info("Client service ~ Login status: " + e.Status.ToString());
@ -209,12 +295,18 @@ namespace SecondBotEvents.Services
client.Network.Disconnected += BotDisconnected; client.Network.Disconnected += BotDisconnected;
client.Network.SimDisconnected += BotSimDisconnected; client.Network.SimDisconnected += BotSimDisconnected;
client.Network.LoginProgress += BotLoginStatus; client.Network.LoginProgress += BotLoginStatus;
client.Self.AlertMessage += BotAlertMessage;
client.Self.AgentDataReply += (s, e) =>
{
master.DataStoreService.SetActiveGroupTitle(e.GroupTitle);
};
} }
public void Login() public void Login()
{ {
ResetClient(); ResetClient();
goodticks = 0; goodticks = 0;
LogoutExpected = false;
master.TriggerBotClientEvent(false, false); master.TriggerBotClientEvent(false, false);
LoginParams loginParams = new( LoginParams loginParams = new(
client, client,
@ -241,6 +333,12 @@ namespace SecondBotEvents.Services
loginParams.ID0 = customId0; loginParams.ID0 = customId0;
} }
string customStart = basicCfg.GetStartLocation();
if (!string.IsNullOrEmpty(customStart))
{
loginParams.Start = customStart;
}
AutoRestartLoginTimer.Start(); AutoRestartLoginTimer.Start();
client.Network.BeginLogin(loginParams); client.Network.BeginLogin(loginParams);
} }

View file

@ -1,4 +1,4 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OpenMetaverse; using OpenMetaverse;
using OpenMetaverse.ImportExport.Collada14; using OpenMetaverse.ImportExport.Collada14;
using SecondBotEvents.Config; using SecondBotEvents.Config;

View file

@ -42,6 +42,7 @@ namespace SecondBotEvents.Services
public DataStoreService(EventsSecondBot setMaster) : base(setMaster) public DataStoreService(EventsSecondBot setMaster) : base(setMaster)
{ {
myConfig = new DataStoreConfig(master.fromEnv,master.fromFolder); myConfig = new DataStoreConfig(master.fromEnv,master.fromFolder);
LoadPendingGroupInvites();
} }
protected Dictionary<string, UUID> avatarsName2Key = []; protected Dictionary<string, UUID> avatarsName2Key = [];
@ -456,12 +457,11 @@ namespace SecondBotEvents.Services
} }
} }
public Dictionary<UUID, string> GetGroupRoles(UUID group) public Dictionary<UUID, string> GetGroupRoles(UUID group, bool force = false)
{ {
Dictionary<UUID, string> reply = []; Dictionary<UUID, string> reply = [];
if (groupRoles.ContainsKey(group) == false) if (force || groupRoles.ContainsKey(group) == false)
{ {
AutoResetEvent fetchEvent = new(false); AutoResetEvent fetchEvent = new(false);
void callback(object sender, GroupRolesDataReplyEventArgs e) void callback(object sender, GroupRolesDataReplyEventArgs e)
{ {
@ -675,6 +675,49 @@ namespace SecondBotEvents.Services
return KnownAvatar(name, UUID.Zero); return KnownAvatar(name, UUID.Zero);
} }
private static readonly string groupInvitesFilePath = "data/pending_group_invites.json";
private void SavePendingGroupInvites()
{
try
{
string dir = System.IO.Path.GetDirectoryName(groupInvitesFilePath);
if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir))
{
System.IO.Directory.CreateDirectory(dir);
}
string json = JsonSerializer.Serialize(PendingGroupInvites, JsonOptions.UnsafeRelaxed);
System.IO.File.WriteAllText(groupInvitesFilePath, json);
}
catch (Exception ex)
{
LogFormater.Crit("Failed to save pending group invites: " + ex.Message);
}
}
private void LoadPendingGroupInvites()
{
try
{
if (System.IO.File.Exists(groupInvitesFilePath))
{
string json = System.IO.File.ReadAllText(groupInvitesFilePath);
var loaded = JsonSerializer.Deserialize<List<GroupInvite>>(json);
if (loaded != null)
{
lock (PendingGroupInvites)
{
PendingGroupInvites = loaded;
}
}
}
}
catch (Exception ex)
{
LogFormater.Crit("Failed to load pending group invites: " + ex.Message);
}
}
public void AddPendingGroupInvite(UUID groupID, UUID agentID, string agentName, string message) public void AddPendingGroupInvite(UUID groupID, UUID agentID, string agentName, string message)
{ {
lock (PendingGroupInvites) lock (PendingGroupInvites)
@ -684,14 +727,50 @@ namespace SecondBotEvents.Services
{ {
return; return;
} }
string groupName = "Unknown Group";
if (GetClient().Groups.GroupName2KeyCache.TryGetValue(groupID, out string resolvedCacheName))
{
groupName = resolvedCacheName;
}
else if (groupsKey2Name.TryGetValue(groupID, out string cachedName))
{
groupName = cachedName;
}
else
{
AutoResetEvent fetchEvent = new(false);
void callback(object sender, GroupNamesEventArgs e)
{
if (e.GroupNames.TryGetValue(groupID, out string resolvedName))
{
groupName = resolvedName;
fetchEvent.Set();
}
}
GetClient().Groups.GroupNamesReply += callback;
GetClient().Groups.RequestGroupName(groupID);
fetchEvent.WaitOne(1500, false);
GetClient().Groups.GroupNamesReply -= callback;
if (groupName != "Unknown Group")
{
lock (groupsKey2Name)
{
groupsKey2Name[groupID] = groupName;
}
}
}
PendingGroupInvites.Add(new GroupInvite PendingGroupInvites.Add(new GroupInvite
{ {
GroupID = groupID, GroupID = groupID,
GroupName = "Unknown Group", // Will be updated on lookup GroupName = groupName,
AgentID = agentID, AgentID = agentID,
AgentName = agentName, AgentName = agentName,
Message = message Message = message
}); });
SavePendingGroupInvites();
} }
} }
@ -708,6 +787,7 @@ namespace SecondBotEvents.Services
lock (PendingGroupInvites) lock (PendingGroupInvites)
{ {
PendingGroupInvites.RemoveAll(i => i.GroupID == groupID); PendingGroupInvites.RemoveAll(i => i.GroupID == groupID);
SavePendingGroupInvites();
} }
} }
@ -1156,12 +1236,35 @@ namespace SecondBotEvents.Services
} }
Dictionary<UUID, Avatar.AvatarProperties> avprops = []; protected string activeGroupTitle = "";
Dictionary<UUID, Dictionary<UUID, string>> avpicks = []; protected Dictionary<UUID, Avatar.AvatarProperties> avprops = [];
protected Dictionary<UUID, Dictionary<UUID, string>> avpicks = [];
protected Dictionary<UUID, Dictionary<UUID, string>> avclassifieds = [];
protected Dictionary<UUID, Dictionary<UUID, ProfilePick>> avpicksDetails = [];
protected Dictionary<UUID, Dictionary<UUID, ClassifiedAd>> avclassifiedsDetails = [];
protected Dictionary<UUID, List<AvatarGroup>> avgroups = [];
public void ClearAvatarPicksCache(UUID agentID)
{
avpicks.Remove(agentID);
lock (avpicksDetails)
{
avpicksDetails.Remove(agentID);
}
}
public void ClearAvatarClassifiedsCache(UUID agentID)
{
avclassifieds.Remove(agentID);
lock (avclassifiedsDetails)
{
avclassifiedsDetails.Remove(agentID);
}
}
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID) public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarPicks(UUID agentID)
{ {
if (avpicks.ContainsKey(agentID) == false) if (!avpicks.ContainsKey(agentID))
{ {
GetClient().Avatars.RequestAvatarPicks(agentID); GetClient().Avatars.RequestAvatarPicks(agentID);
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []); return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
@ -1169,21 +1272,259 @@ namespace SecondBotEvents.Services
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avpicks[agentID]); return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avpicks[agentID]);
} }
public KeyValuePair<bool, Dictionary<UUID, ProfilePick>> GetAvatarPicksDetailed(UUID agentID)
{
if (!avpicks.ContainsKey(agentID))
{
GetClient().Avatars.RequestAvatarPicks(agentID);
return new KeyValuePair<bool, Dictionary<UUID, ProfilePick>>(false, []);
}
lock (avpicksDetails)
{
if (!avpicksDetails.ContainsKey(agentID))
{
avpicksDetails.Add(agentID, []);
foreach (UUID key in avpicks[agentID].Keys)
{
GetClient().Avatars.RequestPickInfo(agentID, key);
}
}
else
{
foreach (UUID pickID in new List<UUID>(avpicksDetails[agentID].Keys))
{
ProfilePick value = avpicksDetails[agentID][pickID];
if (!string.IsNullOrEmpty(value.SimName) || !(value.PosGlobal.X > 0.0) || !(value.PosGlobal.Y > 0.0))
{
continue;
}
float localX;
float localY;
ulong handle = Helpers.GlobalPosToRegionHandle((float)value.PosGlobal.X, (float)value.PosGlobal.Y, out localX, out localY);
if (GetClient().Grid.RegionsByHandle.TryGetValue(handle, out var value2))
{
value.SimName = value2.Name;
avpicksDetails[agentID][pickID] = value;
continue;
}
Task.Run(delegate
{
if (GetClient().Grid.GetGridRegion(handle, GridLayerType.Terrain, out var region))
{
lock (avpicksDetails)
{
if (avpicksDetails.ContainsKey(agentID) && avpicksDetails[agentID].ContainsKey(pickID))
{
ProfilePick value3 = avpicksDetails[agentID][pickID];
value3.SimName = region.Name;
avpicksDetails[agentID][pickID] = value3;
}
}
}
});
}
}
return new KeyValuePair<bool, Dictionary<UUID, ProfilePick>>(true, avpicksDetails[agentID]);
}
}
protected void AvatarPicksReply(object sender, AvatarPicksReplyEventArgs e) protected void AvatarPicksReply(object sender, AvatarPicksReplyEventArgs e)
{ {
lock (avpicks) lock (avpicks)
{ {
if (avpicks.ContainsKey(e.AvatarID) == false) if (!avpicks.ContainsKey(e.AvatarID))
{ {
avpicks.Add(e.AvatarID, []); avpicks.Add(e.AvatarID, []);
} }
avpicks[e.AvatarID] = e.Picks; avpicks[e.AvatarID] = e.Picks;
lock (avpicksDetails)
{
if (!avpicksDetails.ContainsKey(e.AvatarID))
{
avpicksDetails.Add(e.AvatarID, []);
}
else
{
List<UUID> list = [];
foreach (UUID key in avpicksDetails[e.AvatarID].Keys)
{
if (!e.Picks.ContainsKey(key))
{
list.Add(key);
}
}
foreach (UUID item in list)
{
avpicksDetails[e.AvatarID].Remove(item);
}
}
foreach (UUID key2 in e.Picks.Keys)
{
GetClient().Avatars.RequestPickInfo(e.AvatarID, key2);
}
}
}
}
protected void PickInfoReply(object sender, PickInfoReplyEventArgs e)
{
lock (avpicksDetails)
{
UUID avatarID = e.Pick.CreatorID;
if (avatarID == UUID.Zero)
{
lock (avpicks)
{
foreach (KeyValuePair<UUID, Dictionary<UUID, string>> avpick in avpicks)
{
if (avpick.Value.ContainsKey(e.PickID))
{
avatarID = avpick.Key;
break;
}
}
}
}
if (avatarID == UUID.Zero)
{
return;
}
if (!avpicksDetails.ContainsKey(avatarID))
{
avpicksDetails.Add(avatarID, []);
}
ProfilePick pick = e.Pick;
if (string.IsNullOrEmpty(pick.SimName) && pick.PosGlobal.X > 0.0 && pick.PosGlobal.Y > 0.0)
{
float localX;
float localY;
ulong handle = Helpers.GlobalPosToRegionHandle((float)pick.PosGlobal.X, (float)pick.PosGlobal.Y, out localX, out localY);
if (GetClient().Grid.RegionsByHandle.TryGetValue(handle, out var value))
{
pick.SimName = value.Name;
}
else
{
Task.Run(delegate
{
if (GetClient().Grid.GetGridRegion(handle, GridLayerType.Terrain, out var region))
{
lock (avpicksDetails)
{
if (avpicksDetails.ContainsKey(avatarID) && avpicksDetails[avatarID].ContainsKey(e.PickID))
{
ProfilePick value2 = avpicksDetails[avatarID][e.PickID];
value2.SimName = region.Name;
avpicksDetails[avatarID][e.PickID] = value2;
}
}
}
});
}
}
avpicksDetails[avatarID][e.PickID] = pick;
}
}
public KeyValuePair<bool, Dictionary<UUID, string>> GetAvatarClassifieds(UUID agentID)
{
if (!avclassifieds.ContainsKey(agentID))
{
GetClient().Avatars.RequestAvatarClassified(agentID);
return new KeyValuePair<bool, Dictionary<UUID, string>>(false, []);
}
return new KeyValuePair<bool, Dictionary<UUID, string>>(true, avclassifieds[agentID]);
}
public KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>> GetAvatarClassifiedsDetailed(UUID agentID)
{
if (!avclassifieds.ContainsKey(agentID))
{
GetClient().Avatars.RequestAvatarClassified(agentID);
return new KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>>(false, []);
}
lock (avclassifiedsDetails)
{
if (!avclassifiedsDetails.ContainsKey(agentID))
{
avclassifiedsDetails.Add(agentID, []);
foreach (UUID key in avclassifieds[agentID].Keys)
{
GetClient().Avatars.RequestClassifiedInfo(agentID, key);
}
}
return new KeyValuePair<bool, Dictionary<UUID, ClassifiedAd>>(true, avclassifiedsDetails[agentID]);
}
}
protected void AvatarClassifiedsReply(object sender, AvatarClassifiedReplyEventArgs e)
{
lock (avclassifieds)
{
if (!avclassifieds.ContainsKey(e.AvatarID))
{
avclassifieds.Add(e.AvatarID, []);
}
avclassifieds[e.AvatarID] = e.Classifieds;
lock (avclassifiedsDetails)
{
if (!avclassifiedsDetails.ContainsKey(e.AvatarID))
{
avclassifiedsDetails.Add(e.AvatarID, []);
}
else
{
List<UUID> list = [];
foreach (UUID key in avclassifiedsDetails[e.AvatarID].Keys)
{
if (!e.Classifieds.ContainsKey(key))
{
list.Add(key);
}
}
foreach (UUID item in list)
{
avclassifiedsDetails[e.AvatarID].Remove(item);
}
}
foreach (UUID key2 in e.Classifieds.Keys)
{
GetClient().Avatars.RequestClassifiedInfo(e.AvatarID, key2);
}
}
}
}
protected void ClassifiedInfoReply(object sender, ClassifiedInfoReplyEventArgs e)
{
lock (avclassifiedsDetails)
{
UUID uUID = UUID.Zero;
lock (avclassifieds)
{
foreach (KeyValuePair<UUID, Dictionary<UUID, string>> avclassified in avclassifieds)
{
if (avclassified.Value.ContainsKey(e.ClassifiedID))
{
uUID = avclassified.Key;
break;
}
}
}
if (uUID != UUID.Zero)
{
if (!avclassifiedsDetails.ContainsKey(uUID))
{
avclassifiedsDetails.Add(uUID, []);
}
avclassifiedsDetails[uUID][e.ClassifiedID] = e.Classified;
}
} }
} }
public KeyValuePair<bool, Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID) public KeyValuePair<bool, Avatar.AvatarProperties> GetAvatarAvatarProperties(UUID agentID)
{ {
if (avprops.ContainsKey(agentID) == false) if (!avprops.ContainsKey(agentID))
{ {
GetClient().Avatars.RequestAvatarProperties(agentID); GetClient().Avatars.RequestAvatarProperties(agentID);
return new KeyValuePair<bool, Avatar.AvatarProperties>(false, new Avatar.AvatarProperties()); return new KeyValuePair<bool, Avatar.AvatarProperties>(false, new Avatar.AvatarProperties());
@ -1191,15 +1532,51 @@ namespace SecondBotEvents.Services
return new KeyValuePair<bool, Avatar.AvatarProperties>(true, avprops[agentID]); return new KeyValuePair<bool, Avatar.AvatarProperties>(true, avprops[agentID]);
} }
public void UpdateAvatarProperties(UUID agentID, Avatar.AvatarProperties properties)
{
lock (avprops)
{
avprops[agentID] = properties;
}
}
protected void AvatarDetailsReply(object sender, AvatarPropertiesReplyEventArgs e) protected void AvatarDetailsReply(object sender, AvatarPropertiesReplyEventArgs e)
{ {
if(avprops.ContainsKey(e.AvatarID) == false) if (!avprops.ContainsKey(e.AvatarID))
{ {
avprops.Add(e.AvatarID, new Avatar.AvatarProperties()); avprops.Add(e.AvatarID, new Avatar.AvatarProperties());
} }
avprops[e.AvatarID] = e.Properties; avprops[e.AvatarID] = e.Properties;
} }
public KeyValuePair<bool, List<AvatarGroup>> GetAvatarGroups(UUID agentID)
{
if (!avgroups.ContainsKey(agentID))
{
GetClient().Avatars.RequestAvatarProperties(agentID);
return new KeyValuePair<bool, List<AvatarGroup>>(false, []);
}
return new KeyValuePair<bool, List<AvatarGroup>>(true, avgroups[agentID]);
}
protected void AvatarGroupsReplyCallback(object sender, AvatarGroupsReplyEventArgs e)
{
lock (avgroups)
{
avgroups[e.AvatarID] = e.Groups;
}
}
public string GetActiveGroupTitle()
{
return activeGroupTitle;
}
public void SetActiveGroupTitle(string title)
{
activeGroupTitle = title;
}
protected void AttachEventsAfterDelay() protected void AttachEventsAfterDelay()
{ {
GetClient().Groups.CurrentGroups += GroupCurrent; GetClient().Groups.CurrentGroups += GroupCurrent;
@ -1215,7 +1592,11 @@ namespace SecondBotEvents.Services
GetClient().Estate.EstateBansReply += EstateBans; GetClient().Estate.EstateBansReply += EstateBans;
GetClient().Friends.FriendFoundReply += FindFriendReply; GetClient().Friends.FriendFoundReply += FindFriendReply;
GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply; GetClient().Avatars.AvatarPropertiesReply += AvatarDetailsReply;
GetClient().Avatars.AvatarGroupsReply += AvatarGroupsReplyCallback;
GetClient().Avatars.AvatarPicksReply += AvatarPicksReply; GetClient().Avatars.AvatarPicksReply += AvatarPicksReply;
GetClient().Avatars.AvatarClassifiedReply += AvatarClassifiedsReply;
GetClient().Avatars.PickInfoReply += PickInfoReply;
GetClient().Avatars.ClassifiedInfoReply += ClassifiedInfoReply;
GetClient().Groups.RequestCurrentGroups(); GetClient().Groups.RequestCurrentGroups();
_ = GetClient().Self.RetrieveInstantMessages(); _ = GetClient().Self.RetrieveInstantMessages();
if(myConfig.GetPrefetchInventory() == true) if(myConfig.GetPrefetchInventory() == true)
@ -1591,6 +1972,11 @@ namespace SecondBotEvents.Services
GetClient().Estate.EstateBansReply -= EstateBans; GetClient().Estate.EstateBansReply -= EstateBans;
GetClient().Friends.FriendFoundReply -= FindFriendReply; GetClient().Friends.FriendFoundReply -= FindFriendReply;
GetClient().Avatars.AvatarPropertiesReply -= AvatarDetailsReply; GetClient().Avatars.AvatarPropertiesReply -= AvatarDetailsReply;
GetClient().Avatars.AvatarGroupsReply -= AvatarGroupsReplyCallback;
GetClient().Avatars.AvatarPicksReply -= AvatarPicksReply;
GetClient().Avatars.AvatarClassifiedReply -= AvatarClassifiedsReply;
GetClient().Avatars.PickInfoReply -= PickInfoReply;
GetClient().Avatars.ClassifiedInfoReply -= ClassifiedInfoReply;
} }
} }

View file

@ -263,6 +263,7 @@ namespace SecondBotEvents.Services
{ {
GetClient().Self.ScriptDialog -= DialogWindowEvent; GetClient().Self.ScriptDialog -= DialogWindowEvent;
GetClient().Self.LoadURL -= LoadUrlEvent; GetClient().Self.LoadURL -= LoadUrlEvent;
GetClient().Self.AlertMessage -= BotAlertMessage;
} }
} }
@ -273,6 +274,23 @@ namespace SecondBotEvents.Services
// attach events // attach events
GetClient().Self.ScriptDialog += DialogWindowEvent; GetClient().Self.ScriptDialog += DialogWindowEvent;
GetClient().Self.LoadURL += LoadUrlEvent; GetClient().Self.LoadURL += LoadUrlEvent;
GetClient().Self.AlertMessage += BotAlertMessage;
}
public void Attach()
{
botConnected = true;
// attach events
GetClient().Self.ScriptDialog += DialogWindowEvent;
GetClient().Self.LoadURL += LoadUrlEvent;
GetClient().Self.AlertMessage += BotAlertMessage;
}
protected void BotAlertMessage(object sender, AlertMessageEventArgs e)
{
string message = " [SYSTEM] ALERT: " + e.Message;
master.DataStoreService.BotRecordLocalchatReply(message, "[SYSTEM] ");
LogFormater.Info("[AlertMessage] " + e.Message);
} }
protected void LoadUrlEvent(object sender, LoadUrlEventArgs e) protected void LoadUrlEvent(object sender, LoadUrlEventArgs e)

View file

@ -1,4 +1,4 @@
using Discord; using Discord;
using Discord.Rest; using Discord.Rest;
using Discord.WebSocket; using Discord.WebSocket;
using OpenMetaverse; using OpenMetaverse;
@ -1300,7 +1300,7 @@ namespace SecondBotEvents.Services
SocketRole role = server.GetRole(roleid); SocketRole role = server.GetRole(roleid);
Dictionary<string, string> settings = new() Dictionary<string, string> settings = new()
{ {
{ "Config|Color", String.Join(",", [role.Color.R, role.Color.G, role.Color.B]) }, { "Config|Color", $"{role.Color.R},{role.Color.G},{role.Color.B}" },
{ "Config|Name", role.Name }, { "Config|Name", role.Name },
{ "Info|AssignedCount", role.Members.ToString() }, { "Info|AssignedCount", role.Members.ToString() },
{ "Perm|AttachFiles", role.Permissions.AttachFiles.ToString() }, { "Perm|AttachFiles", role.Permissions.AttachFiles.ToString() },

View file

@ -127,9 +127,11 @@ namespace SecondBotEvents.Services
private void CreateWebServer() private void CreateWebServer()
{ {
Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>(); // Swan.Logging.Logger.UnregisterLogger<ConsoleLogger>();
string hostName = System.Net.Dns.GetHostName();
int port = myConfig.GetPort();
HTTPendpoint = new WebServer(o => o HTTPendpoint = new WebServer(o => o
.WithUrlPrefix("http://*:80") .WithUrlPrefix($"http://{hostName}:{port}/")
.WithMode(HttpListenerMode.EmbedIO)) .WithMode(HttpListenerMode.EmbedIO))
.WithCors() .WithCors()
.WithWebApi("/api", SerializationCallback, m => m.WithController(() => new HttpWebBot(master))) .WithWebApi("/api", SerializationCallback, m => m.WithController(() => new HttpWebBot(master)))
@ -137,7 +139,7 @@ namespace SecondBotEvents.Services
.WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" })) .WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new { Message = "Error" }))
); );
HTTPendpoint.RunAsync(); HTTPendpoint.RunAsync();
LogFormater.Info("HTTP service [Enabled] on port 80"); LogFormater.Info($"HTTP service [Enabled] on {hostName}:{port}");
} }
protected string JsonInput(IHttpContext httpContext) protected string JsonInput(IHttpContext httpContext)
@ -399,9 +401,9 @@ namespace SecondBotEvents.Services
public string Run([FormField] string commandName, [FormField] string args, [FormField] string signing, [FormField] string unixtime) public string Run([FormField] string commandName, [FormField] string args, [FormField] string signing, [FormField] string unixtime)
{ {
string[] myArgs = []; string[] myArgs = [];
if(args != null) if(!string.IsNullOrEmpty(args))
{ {
myArgs = args.Split("~#~", StringSplitOptions.RemoveEmptyEntries); myArgs = args.Split("~#~", StringSplitOptions.None);
} }
if(int.TryParse(unixtime, out int cmdUnixtime) == false) if(int.TryParse(unixtime, out int cmdUnixtime) == false)
{ {

View file

@ -1,4 +1,4 @@
using SecondBotEvents.Config; using SecondBotEvents.Config;
using RabbitMQ.Client; using RabbitMQ.Client;
using System; using System;
using System.Text; using System.Text;
@ -209,6 +209,7 @@ namespace SecondBotEvents.Services
connection.CloseAsync().Await(); connection.CloseAsync().Await();
connection = null; connection = null;
} }
channels.Clear();
var endpoints = new System.Collections.Generic.List<AmqpTcpEndpoint> { var endpoints = new System.Collections.Generic.List<AmqpTcpEndpoint> {
new AmqpTcpEndpoint(myConfig.GetHostIP(),myConfig.GetHostPort()), new AmqpTcpEndpoint(myConfig.GetHostIP(),myConfig.GetHostPort()),
}; };