Files
libremetaverse/LibreMetaverse/FriendsManager.cs
2025-06-12 20:25:08 -05:00

1180 lines
49 KiB
C#

/*
* Copyright (c) 2006-2016, openmetaverse.co
* Copyright (c) 2022-2025, Sjofn LLC.
* All rights reserved.
*
* - Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Neither the name of the openmetaverse.co nor the names
* of its contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenMetaverse.Packets;
using OpenMetaverse.StructuredData;
namespace OpenMetaverse
{
[Flags]
public enum FriendRights : int
{
/// <summary>The avatar has no rights</summary>
None = 0,
/// <summary>The avatar can see the online status of the target avatar</summary>
CanSeeOnline = 1,
/// <summary>The avatar can see the location of the target avatar on the map</summary>
CanSeeOnMap = 2,
/// <summary>The avatar can modify the objects of the target avatar </summary>
CanModifyObjects = 4
}
/// <summary>
/// This class holds information about an avatar in the friends list. There are two ways
/// to interface to this class. The first is through the set of boolean properties. This is the typical
/// way clients of this class will use it. The second interface is through two bitflag properties,
/// TheirFriendsRights and MyFriendsRights
/// </summary>
public class FriendInfo
{
private bool m_canSeeMeOnline;
private bool m_canSeeMeOnMap;
#region Properties
/// <summary>
/// System ID of the avatar
/// </summary>
public UUID UUID { get; }
/// <summary>
/// full name of the avatar
/// </summary>
public string Name { get; set; }
/// <summary>
/// True if the avatar is online
/// </summary>
public bool IsOnline { get; set; }
/// <summary>
/// True if the friend can see if I am online
/// </summary>
public bool CanSeeMeOnline
{
get { return m_canSeeMeOnline; }
set
{
m_canSeeMeOnline = value;
// if I can't see them online, then I can't see them on the map
if (!m_canSeeMeOnline)
m_canSeeMeOnMap = false;
}
}
/// <summary>
/// True if the friend can see me on the map
/// </summary>
public bool CanSeeMeOnMap
{
get { return m_canSeeMeOnMap; }
set
{
// if I can't see them online, then I can't see them on the map
if (m_canSeeMeOnline)
m_canSeeMeOnMap = value;
}
}
/// <summary>
/// True if the freind can modify my objects
/// </summary>
public bool CanModifyMyObjects { get; set; }
/// <summary>
/// True if I can see if my friend is online
/// </summary>
public bool CanSeeThemOnline { get; private set; }
/// <summary>
/// True if I can see if my friend is on the map
/// </summary>
public bool CanSeeThemOnMap { get; private set; }
/// <summary>
/// True if I can modify my friend's objects
/// </summary>
public bool CanModifyTheirObjects { get; private set; }
/// <summary>
/// My friend's rights represented as bitmapped flags
/// </summary>
public FriendRights TheirFriendRights
{
get
{
FriendRights results = FriendRights.None;
if (m_canSeeMeOnline)
results |= FriendRights.CanSeeOnline;
if (m_canSeeMeOnMap)
results |= FriendRights.CanSeeOnMap;
if (CanModifyMyObjects)
results |= FriendRights.CanModifyObjects;
return results;
}
set
{
m_canSeeMeOnline = (value & FriendRights.CanSeeOnline) != 0;
m_canSeeMeOnMap = (value & FriendRights.CanSeeOnMap) != 0;
CanModifyMyObjects = (value & FriendRights.CanModifyObjects) != 0;
}
}
/// <summary>
/// My rights represented as bitmapped flags
/// </summary>
public FriendRights MyFriendRights
{
get
{
FriendRights results = FriendRights.None;
if (CanSeeThemOnline)
results |= FriendRights.CanSeeOnline;
if (CanSeeThemOnMap)
results |= FriendRights.CanSeeOnMap;
if (CanModifyTheirObjects)
results |= FriendRights.CanModifyObjects;
return results;
}
set
{
CanSeeThemOnline = (value & FriendRights.CanSeeOnline) != 0;
CanSeeThemOnMap = (value & FriendRights.CanSeeOnMap) != 0;
CanModifyTheirObjects = (value & FriendRights.CanModifyObjects) != 0;
}
}
#endregion Properties
/// <summary>
/// Used internally when building the initial list of friends at login time
/// </summary>
/// <param name="id">System ID of the avatar being represented</param>
/// <param name="theirRights">Rights the friend has to see you online and to modify your objects</param>
/// <param name="myRights">Rights you have to see your friend online and to modify their objects</param>
internal FriendInfo(UUID id, FriendRights theirRights, FriendRights myRights)
{
UUID = id;
m_canSeeMeOnline = (theirRights & FriendRights.CanSeeOnline) != 0;
m_canSeeMeOnMap = (theirRights & FriendRights.CanSeeOnMap) != 0;
CanModifyMyObjects = (theirRights & FriendRights.CanModifyObjects) != 0;
CanSeeThemOnline = (myRights & FriendRights.CanSeeOnline) != 0;
CanSeeThemOnMap = (myRights & FriendRights.CanSeeOnMap) != 0;
CanModifyTheirObjects = (myRights & FriendRights.CanModifyObjects) != 0;
}
/// <summary>
/// FriendInfo represented as a string
/// </summary>
/// <returns><see cref="string"/> representation of both my rights and my friends rights</returns>
public override string ToString()
{
return !string.IsNullOrEmpty(Name)
? $"{Name} (Their Rights: {TheirFriendRights}, My Rights: {MyFriendRights})"
: $"{UUID} (Their Rights: {TheirFriendRights}, My Rights: {MyFriendRights})";
}
}
/// <summary>
/// This class is used to add and remove avatars from your friends list and to manage their permission.
/// </summary>
public class FriendsManager
{
#region Delegates
private EventHandler<FriendsReadyEventArgs> m_FriendsListReadyResponse;
protected virtual void OnfriendsListReady(FriendsReadyEventArgs e)
{
EventHandler<FriendsReadyEventArgs> handler = m_FriendsListReadyResponse;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendsListReadyLock = new object();
public event EventHandler<FriendsReadyEventArgs> friendsListReady
{
add { lock (m_FriendsListReadyLock) { m_FriendsListReadyResponse += value; } }
remove { lock (m_FriendsListReadyLock) { m_FriendsListReadyResponse -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendInfoEventArgs> m_FriendOnline;
/// <summary>Raises the FriendOnline event</summary>
/// <param name="e">A FriendInfoEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendOnline(FriendInfoEventArgs e)
{
EventHandler<FriendInfoEventArgs> handler = m_FriendOnline;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendOnlineLock = new object();
/// <summary>Raised when the simulator sends notification one of the members in our friends list comes online</summary>
public event EventHandler<FriendInfoEventArgs> FriendOnline
{
add { lock (m_FriendOnlineLock) { m_FriendOnline += value; } }
remove { lock (m_FriendOnlineLock) { m_FriendOnline -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendInfoEventArgs> m_FriendOffline;
/// <summary>Raises the FriendOffline event</summary>
/// <param name="e">A FriendInfoEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendOffline(FriendInfoEventArgs e)
{
EventHandler<FriendInfoEventArgs> handler = m_FriendOffline;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendOfflineLock = new object();
/// <summary>Raised when the simulator sends notification one of the members in our friends list goes offline</summary>
public event EventHandler<FriendInfoEventArgs> FriendOffline
{
add { lock (m_FriendOfflineLock) { m_FriendOffline += value; } }
remove { lock (m_FriendOfflineLock) { m_FriendOffline -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendInfoEventArgs> m_FriendRights;
/// <summary>Raises the FriendRightsUpdate event</summary>
/// <param name="e">A FriendInfoEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendRights(FriendInfoEventArgs e)
{
EventHandler<FriendInfoEventArgs> handler = m_FriendRights;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendRightsLock = new object();
/// <summary>Raised when the simulator sends notification one of the members in our friends list grants or revokes permissions</summary>
public event EventHandler<FriendInfoEventArgs> FriendRightsUpdate
{
add { lock (m_FriendRightsLock) { m_FriendRights += value; } }
remove { lock (m_FriendRightsLock) { m_FriendRights -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendNamesEventArgs> m_FriendNames;
/// <summary>Raises the FriendNames event</summary>
/// <param name="e">A FriendNamesEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendNames(FriendNamesEventArgs e)
{
EventHandler<FriendNamesEventArgs> handler = m_FriendNames;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendNamesLock = new object();
/// <summary>Raised when the simulator sends us the names on our friends list</summary>
public event EventHandler<FriendNamesEventArgs> FriendNames
{
add { lock (m_FriendNamesLock) { m_FriendNames += value; } }
remove { lock (m_FriendNamesLock) { m_FriendNames -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendshipOfferedEventArgs> m_FriendshipOffered;
/// <summary>Raises the FriendshipOffered event</summary>
/// <param name="e">A FriendshipOfferedEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendshipOffered(FriendshipOfferedEventArgs e)
{
EventHandler<FriendshipOfferedEventArgs> handler = m_FriendshipOffered;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendshipOfferedLock = new object();
/// <summary>Raised when the simulator sends notification another agent is offering us friendship</summary>
public event EventHandler<FriendshipOfferedEventArgs> FriendshipOffered
{
add { lock (m_FriendshipOfferedLock) { m_FriendshipOffered += value; } }
remove { lock (m_FriendshipOfferedLock) { m_FriendshipOffered -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendshipResponseEventArgs> m_FriendshipResponse;
/// <summary>Raises the FriendshipResponse event</summary>
/// <param name="e">A FriendshipResponseEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendshipResponse(FriendshipResponseEventArgs e)
{
EventHandler<FriendshipResponseEventArgs> handler = m_FriendshipResponse;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendshipResponseLock = new object();
/// <summary>Raised when a request we sent to friend another agent is accepted or declined</summary>
public event EventHandler<FriendshipResponseEventArgs> FriendshipResponse
{
add { lock (m_FriendshipResponseLock) { m_FriendshipResponse += value; } }
remove { lock (m_FriendshipResponseLock) { m_FriendshipResponse -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendshipTerminatedEventArgs> m_FriendshipTerminated;
/// <summary>Raises the FriendshipTerminated event</summary>
/// <param name="e">A FriendshipTerminatedEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendshipTerminated(FriendshipTerminatedEventArgs e)
{
EventHandler<FriendshipTerminatedEventArgs> handler = m_FriendshipTerminated;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendshipTerminatedLock = new object();
/// <summary>Raised when the simulator sends notification one of the members in our friends list has terminated
/// our friendship</summary>
public event EventHandler<FriendshipTerminatedEventArgs> FriendshipTerminated
{
add { lock (m_FriendshipTerminatedLock) { m_FriendshipTerminated += value; } }
remove { lock (m_FriendshipTerminatedLock) { m_FriendshipTerminated -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<FriendFoundReplyEventArgs> m_FriendFound;
/// <summary>Raises the FriendFoundReply event</summary>
/// <param name="e">A FriendFoundReplyEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnFriendFoundReply(FriendFoundReplyEventArgs e)
{
EventHandler<FriendFoundReplyEventArgs> handler = m_FriendFound;
handler?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_FriendFoundLock = new object();
/// <summary>Raised when the simulator sends the location of a friend we have
/// requested map location info for</summary>
public event EventHandler<FriendFoundReplyEventArgs> FriendFoundReply
{
add { lock (m_FriendFoundLock) { m_FriendFound += value; } }
remove { lock (m_FriendFoundLock) { m_FriendFound -= value; } }
}
#endregion Delegates
private readonly GridClient Client;
/// <summary>
/// A dictionary of key/value pairs containing known friends of this avatar.
///
/// The Key is the <see cref="UUID"/> of the friend, the value is a <see cref="FriendInfo"/>
/// object that contains detailed information including permissions you have and have given to the friend
/// </summary>
public ConcurrentDictionary<UUID, FriendInfo> FriendList = new ConcurrentDictionary<UUID, FriendInfo>();
/// <summary>
/// A Dictionary of key/value pairs containing current pending friendship offers.
///
/// The key is the <see cref="UUID"/> of the avatar making the request,
/// the value is the <see cref="UUID"/> of the request which is used to accept
/// or decline the friendship offer
/// </summary>
public ConcurrentDictionary<UUID, UUID> FriendRequests = new ConcurrentDictionary<UUID, UUID>();
/// <summary>
/// Internal constructor
/// </summary>
/// <param name="client">A reference to the GridClient Object</param>
internal FriendsManager(GridClient client)
{
Client = client;
Client.Network.LoginProgress += Network_OnConnect;
Client.Avatars.UUIDNameReply += new EventHandler<UUIDNameReplyEventArgs>(Avatars_OnAvatarNames);
Client.Self.IM += Self_IM;
Client.Network.RegisterCallback(PacketType.OnlineNotification, OnlineNotificationHandler);
Client.Network.RegisterCallback(PacketType.OfflineNotification, OfflineNotificationHandler);
Client.Network.RegisterCallback(PacketType.ChangeUserRights, ChangeUserRightsHandler);
Client.Network.RegisterCallback(PacketType.TerminateFriendship, TerminateFriendshipHandler);
Client.Network.RegisterCallback(PacketType.FindAgent, OnFindAgentReplyHandler);
Client.Network.RegisterLoginResponseCallback(Network_OnLoginResponse, new[] { "buddy-list" });
}
#region Public Methods
/// <summary>
/// Accept a friendship request via LLUDP packet
/// </summary>
/// <param name="fromAgentID">agentID of avatar to form friendship with</param>
/// <param name="imSessionID">imSessionID of the friendship request message</param>
public void AcceptFriendship(UUID fromAgentID, UUID imSessionID)
{
UUID callingCardFolder = Client.Inventory.FindFolderForType(AssetType.CallingCard);
AcceptFriendshipPacket request = new AcceptFriendshipPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
TransactionBlock =
{
TransactionID = imSessionID
},
FolderData = new AcceptFriendshipPacket.FolderDataBlock[1]
};
request.FolderData[0] = new AcceptFriendshipPacket.FolderDataBlock
{
FolderID = callingCardFolder
};
Client.Network.SendPacket(request);
FriendInfo friend = new FriendInfo(fromAgentID, FriendRights.CanSeeOnline,
FriendRights.CanSeeOnline);
FriendList.TryAdd(friend.UUID, friend);
FriendRequests.TryRemove(fromAgentID, out _);
Client.Avatars.RequestAvatarName(fromAgentID);
}
/// <summary>
/// Accept friendship request. Only to be used if request was sent via Offline Msg cap
/// This can be determined by the presence of a <see cref="InstantMessageEventArgs.Simulator"/>
/// value in <see cref="InstantMessageEventArgs" />
/// </summary>
/// <param name="fromAgentID">agentID of avatar to form friendship with</param>
/// <param name="cancellationToken"></param>
public async Task AcceptFriendshipViaCapAsync(UUID fromAgentID, CancellationToken cancellationToken = default)
{
Uri acceptFriendshipCap = Client.Network.CurrentSim.Caps.CapabilityURI("AcceptFriendship");
if (acceptFriendshipCap == null)
{
Logger.Log("AcceptFriendship capability not found.", Helpers.LogLevel.Warning);
return;
}
UriBuilder builder = new UriBuilder(acceptFriendshipCap)
{
// Second Life has some infinitely stupid escaped agent name as part of the uri query.
// Hopefully we don't need it because it makes no sense at all. Period, but just in case:
// ?from={fromAgentID}&agent_name=\"This%20Sucks\"
Query = $"from={fromAgentID}"
};
acceptFriendshipCap = builder.Uri;
await Client.HttpCapsClient.PostRequestAsync(acceptFriendshipCap, OSDFormat.Xml, new OSD(), cancellationToken,
(response, data, error) =>
{
if (error != null)
{
Logger.Log($"AcceptFriendship failed for {fromAgentID}. ({error.Message})",
Helpers.LogLevel.Warning);
return;
}
OSD result = OSDParser.Deserialize(data);
if (result is OSDMap resMap && resMap.ContainsKey("success") && resMap["success"].AsBoolean())
{
FriendInfo friend = new FriendInfo(fromAgentID, FriendRights.CanSeeOnline, FriendRights.CanSeeOnline);
FriendList.TryAdd(friend.UUID, friend);
FriendRequests.TryRemove(fromAgentID, out _);
Client.Avatars.RequestAvatarName(fromAgentID);
}
});
}
/// <summary>
/// Decline a friendship request via LLUDP packet
/// </summary>
/// <param name="fromAgentID"><see cref="UUID"/> of friend</param>
/// <param name="imSessionID">imSessionID of the friendship request message</param>
public void DeclineFriendship(UUID fromAgentID, UUID imSessionID)
{
DeclineFriendshipPacket request = new DeclineFriendshipPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
TransactionBlock =
{
TransactionID = imSessionID
}
};
Client.Network.SendPacket(request);
FriendRequests.TryRemove(fromAgentID, out _);
}
/// <summary>
/// Decline friendship request. Only to be used if request was sent via Offline Msg cap
/// This can be determined by the presence of a <see cref="InstantMessageEventArgs.Simulator"/>
/// value in <see cref="InstantMessageEventArgs" />
/// </summary>
/// <param name="fromAgentID"><see cref="UUID"/> of friend</param>
/// <param name="cancellationToken"></param>
public async Task DeclineFriendshipViaCapAsync(UUID fromAgentID, CancellationToken cancellationToken = default)
{
Uri declineFriendshipCap = Client.Network.CurrentSim.Caps.CapabilityURI("DeclineFriendship");
if (declineFriendshipCap == null)
{
Logger.Log("DeclineFriendship capability not found.", Helpers.LogLevel.Warning);
return;
}
UriBuilder builder = new UriBuilder(declineFriendshipCap)
{
Query = $"from={fromAgentID}"
};
declineFriendshipCap = builder.Uri;
await Client.HttpCapsClient.DeleteRequestAsync(declineFriendshipCap, OSDFormat.Xml, new OSD(), cancellationToken,
(response, data, error) =>
{
if (error != null)
{
Logger.Log($"DeclineFriendship failed for {fromAgentID}. ({error.Message})",
Helpers.LogLevel.Warning);
return;
}
OSD result = OSDParser.Deserialize(data);
if (result is OSDMap resMap && resMap.ContainsKey("success") && resMap["success"].AsBoolean())
{
FriendRequests.TryRemove(fromAgentID, out _);
}
});
}
/// <summary>
/// Overload: Offer friendship to an avatar.
/// </summary>
/// <param name="agentID">System ID of the avatar you are offering friendship to</param>
public void OfferFriendship(UUID agentID)
{
OfferFriendship(agentID, "Do ya wanna be my buddy?");
}
/// <summary>
/// Offer friendship to an avatar.
/// </summary>
/// <param name="agentID">System ID of the avatar you are offering friendship to</param>
/// <param name="message">A message to send with the request</param>
public void OfferFriendship(UUID agentID, string message)
{
Client.Self.InstantMessage(Client.Self.Name,
agentID,
message,
UUID.Random(),
InstantMessageDialog.FriendshipOffered,
InstantMessageOnline.Offline,
Client.Self.SimPosition,
Client.Network.CurrentSim.ID,
null);
}
/// <summary>
/// Terminate a friendship with an avatar
/// </summary>
/// <param name="agentID">System ID of the avatar you are terminating the friendship with</param>
public void TerminateFriendship(UUID agentID)
{
if (FriendList.ContainsKey(agentID))
{
TerminateFriendshipPacket request = new TerminateFriendshipPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
ExBlock =
{
OtherID = agentID
}
};
Client.Network.SendPacket(request);
FriendList.TryRemove(agentID, out _);
}
}
/// <summary>Process an incoming packet and raise the appropriate events</summary>
/// <param name="sender">The sender</param>
/// <param name="e">The EventArgs object containing the packet data</param>
private void TerminateFriendshipHandler(object sender, PacketReceivedEventArgs e)
{
Packet packet = e.Packet;
TerminateFriendshipPacket itsOver = (TerminateFriendshipPacket)packet;
string name = string.Empty;
if (FriendList.TryGetValue(itsOver.ExBlock.OtherID, out var friend))
{
name = friend.Name;
FriendList.TryRemove(itsOver.ExBlock.OtherID, out _);
}
if (m_FriendshipTerminated != null)
{
OnFriendshipTerminated(new FriendshipTerminatedEventArgs(itsOver.ExBlock.OtherID, name));
}
}
/// <summary>
/// Change the rights of a friend avatar.
/// </summary>
/// <param name="friendID">the <see cref="UUID"/> of the friend</param>
/// <param name="rights">the new rights to give the friend</param>
/// <remarks>This method will implicitly set the rights to those passed in the rights parameter.</remarks>
public void GrantRights(UUID friendID, FriendRights rights)
{
GrantUserRightsPacket request = new GrantUserRightsPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
Rights = new GrantUserRightsPacket.RightsBlock[1]
};
request.Rights[0] = new GrantUserRightsPacket.RightsBlock
{
AgentRelated = friendID,
RelatedRights = (int)rights
};
Client.Network.SendPacket(request);
}
/// <summary>
/// Use to map a friends location on the grid.
/// </summary>
/// <param name="friendID">Friends UUID to find</param>
/// <remarks><see cref="E:OnFriendFound"/></remarks>
public void MapFriend(UUID friendID)
{
FindAgentPacket stalk = new FindAgentPacket
{
AgentBlock =
{
Hunter = Client.Self.AgentID,
Prey = friendID,
SpaceIP = 0 // Will be filled in by the simulator
},
LocationBlock = new FindAgentPacket.LocationBlockBlock[1]
};
stalk.LocationBlock[0] = new FindAgentPacket.LocationBlockBlock
{
GlobalX = 0.0, // Filled in by the simulator
GlobalY = 0.0
};
Client.Network.SendPacket(stalk);
}
/// <summary>
/// Use to track a friends movement on the grid
/// </summary>
/// <param name="friendID">Friends Key</param>
public void TrackFriend(UUID friendID)
{
TrackAgentPacket stalk = new TrackAgentPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
TargetData =
{
PreyID = friendID
}
};
Client.Network.SendPacket(stalk);
}
/// <summary>
/// Ask for a notification of friend's online status
/// </summary>
/// <param name="friendID">Friend's UUID</param>
public void RequestOnlineNotification(UUID friendID)
{
GenericMessagePacket gmp = new GenericMessagePacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID,
TransactionID = UUID.Zero
},
MethodData =
{
Method = Utils.StringToBytes("requestonlinenotification"),
Invoice = UUID.Zero
},
ParamList = new GenericMessagePacket.ParamListBlock[1]
};
gmp.ParamList[0] = new GenericMessagePacket.ParamListBlock
{
Parameter = Utils.StringToBytes(friendID.ToString())
};
Client.Network.SendPacket(gmp);
}
#endregion
#region Internal events
private void Network_OnConnect(object sender, LoginProgressEventArgs e)
{
if (e.Status != LoginStatus.Success)
{
return;
}
var names = (from friend in FriendList
where friend.Value != null && string.IsNullOrEmpty(friend.Value.Name)
select friend.Key).ToList();
if (names.Any())
{
Client.Avatars.RequestAvatarNames(names);
}
}
/// <summary>
/// This handles the asynchronous response of a RequestAvatarNames call.
/// </summary>
/// <param name="sender"></param>
/// <param name="e">names corresponding to the list of IDs sent to RequestAvatarNames.</param>
private void Avatars_OnAvatarNames(object sender, UUIDNameReplyEventArgs e)
{
var newNames = new Dictionary<UUID, string>();
foreach (var kvp in e.Names)
{
if (FriendList.TryGetValue(kvp.Key, out var friend))
{
if (friend.Name == null)
{
newNames.Add(kvp.Key, e.Names[kvp.Key]);
}
friend.Name = e.Names[kvp.Key];
FriendList[kvp.Key] = friend;
}
}
if (newNames.Count > 0 && m_FriendNames != null)
{
OnFriendNames(new FriendNamesEventArgs(newNames));
}
}
#endregion
#region Packet Handlers
/// <summary>Process an incoming packet and raise the appropriate events</summary>
/// <param name="sender">The sender</param>
/// <param name="e">The EventArgs object containing the packet data</param>
protected void OnlineNotificationHandler(object sender, PacketReceivedEventArgs e)
{
Packet packet = e.Packet;
if (packet.Type == PacketType.OnlineNotification)
{
OnlineNotificationPacket notification = ((OnlineNotificationPacket)packet);
foreach (OnlineNotificationPacket.AgentBlockBlock block in notification.AgentBlock)
{
FriendInfo friend;
if (!FriendList.ContainsKey(block.AgentID))
{
friend = new FriendInfo(block.AgentID, FriendRights.CanSeeOnline,
FriendRights.CanSeeOnline);
FriendList.TryAdd(block.AgentID, friend);
}
else
{
friend = FriendList[block.AgentID];
}
bool doNotify = !friend.IsOnline;
friend.IsOnline = true;
if (m_FriendOnline != null && doNotify)
{
OnFriendOnline(new FriendInfoEventArgs(friend));
}
}
}
}
/// <summary>Process an incoming packet and raise the appropriate events</summary>
/// <param name="sender">The sender</param>
/// <param name="e">The EventArgs object containing the packet data</param>
protected void OfflineNotificationHandler(object sender, PacketReceivedEventArgs e)
{
Packet packet = e.Packet;
if (packet.Type == PacketType.OfflineNotification)
{
OfflineNotificationPacket notification = (OfflineNotificationPacket)packet;
foreach (OfflineNotificationPacket.AgentBlockBlock block in notification.AgentBlock)
{
FriendInfo friend = new FriendInfo(block.AgentID, FriendRights.CanSeeOnline, FriendRights.CanSeeOnline);
FriendList.TryAdd(block.AgentID, friend);
if (FriendList.TryGetValue(block.AgentID, out friend))
{
friend.IsOnline = false;
if (m_FriendOffline != null)
{
OnFriendOffline(new FriendInfoEventArgs(friend));
}
}
}
}
}
/// <summary>Process an incoming packet and raise the appropriate events</summary>
/// <param name="sender">The sender</param>
/// <param name="e">The EventArgs object containing the packet data</param>
private void ChangeUserRightsHandler(object sender, PacketReceivedEventArgs e)
{
Packet packet = e.Packet;
if (packet.Type == PacketType.ChangeUserRights)
{
ChangeUserRightsPacket rights = (ChangeUserRightsPacket)packet;
foreach (ChangeUserRightsPacket.RightsBlock block in rights.Rights)
{
FriendRights newRights = (FriendRights)block.RelatedRights;
if (FriendList.TryGetValue(block.AgentRelated, out var friend))
{
friend.TheirFriendRights = newRights;
if (m_FriendRights != null)
{
OnFriendRights(new FriendInfoEventArgs(friend));
}
}
else if (block.AgentRelated == Client.Self.AgentID)
{
if (FriendList.TryGetValue(rights.AgentData.AgentID, out friend))
{
friend.MyFriendRights = newRights;
if (m_FriendRights != null)
{
OnFriendRights(new FriendInfoEventArgs(friend));
}
}
}
}
}
}
/// <summary>Process an incoming packet and raise the appropriate events</summary>
/// <param name="sender">The sender</param>
/// <param name="e">The EventArgs object containing the packet data</param>
public void OnFindAgentReplyHandler(object sender, PacketReceivedEventArgs e)
{
if (m_FriendFound != null)
{
Packet packet = e.Packet;
FindAgentPacket reply = (FindAgentPacket)packet;
UUID prey = reply.AgentBlock.Prey;
ulong regionHandle = Helpers.GlobalPosToRegionHandle((float)reply.LocationBlock[0].GlobalX,
(float)reply.LocationBlock[0].GlobalY, out var x, out var y);
Vector3 xyz = new Vector3(x, y, 0f);
OnFriendFoundReply(new FriendFoundReplyEventArgs(prey, regionHandle, xyz));
}
}
#endregion
private void Self_IM(object sender, InstantMessageEventArgs e)
{
if (e.IM.Dialog == InstantMessageDialog.FriendshipOffered)
{
if (m_FriendshipOffered != null)
{
FriendRequests.TryAdd(e.IM.FromAgentID, e.IM.IMSessionID);
OnFriendshipOffered(new FriendshipOfferedEventArgs(e.IM.FromAgentID, e.IM.FromAgentName, e.IM.IMSessionID));
}
}
else if (e.IM.Dialog == InstantMessageDialog.FriendshipAccepted)
{
FriendInfo friend = new FriendInfo(e.IM.FromAgentID, FriendRights.CanSeeOnline,
FriendRights.CanSeeOnline)
{
Name = e.IM.FromAgentName
};
FriendList[friend.UUID] = friend;
if (m_FriendshipResponse != null)
{
OnFriendshipResponse(new FriendshipResponseEventArgs(e.IM.FromAgentID, e.IM.FromAgentName, true));
}
RequestOnlineNotification(e.IM.FromAgentID);
}
else if (e.IM.Dialog == InstantMessageDialog.FriendshipDeclined)
{
if (m_FriendshipResponse != null)
{
OnFriendshipResponse(new FriendshipResponseEventArgs(e.IM.FromAgentID, e.IM.FromAgentName, false));
}
}
}
/// <summary>
/// Populate FriendList <see cref="LockingDictionary{TKey,TValue}"/> with data from the login reply
/// </summary>
/// <param name="loginSuccess">true if login was successful</param>
/// <param name="redirect">true if login request is requiring a redirect</param>
/// <param name="message">A string containing the response to the login request</param>
/// <param name="reason">A string containing the reason for the request</param>
/// <param name="replyData">A <see cref="LoginResponseData"/> object containing the decoded
/// reply from the login server</param>
private void Network_OnLoginResponse(bool loginSuccess, bool redirect, string message, string reason,
LoginResponseData replyData)
{
int uuidLength = UUID.Zero.ToString().Length;
if (loginSuccess && replyData.BuddyList != null)
{
foreach (BuddyListEntry buddy in replyData.BuddyList)
{
string id = buddy.BuddyId.Length > uuidLength ? buddy.BuddyId.Substring(0, uuidLength) : buddy.BuddyId;
if (UUID.TryParse(id, out var bubid))
{
_ = FriendList.TryAdd(bubid, new FriendInfo(bubid,
(FriendRights)buddy.BuddyRightsGiven,
(FriendRights)buddy.BuddyRightsHas));
}
}
OnfriendsListReady(new FriendsReadyEventArgs(FriendList.Count));
}
}
}
#region EventArgs
public class FriendsReadyEventArgs : EventArgs
{
/// <summary>Number of friends we have</summary>
public int Count { get; }
/// <summary>Get the name of the agent we requested a friendship with</summary>
/// <summary>
/// Construct a new instance of the FriendsReadyEventArgs class
/// </summary>
/// <param name="count">The total number of people loaded into the friend list.</param>
public FriendsReadyEventArgs(int count)
{
this.Count = count;
}
}
/// <summary>Contains information on a member of our friends list</summary>
public class FriendInfoEventArgs : EventArgs
{
/// <summary>Get the FriendInfo</summary>
public FriendInfo Friend { get; }
/// <summary>
/// Construct a new instance of the FriendInfoEventArgs class
/// </summary>
/// <param name="friend">The FriendInfo</param>
public FriendInfoEventArgs(FriendInfo friend)
{
this.Friend = friend;
}
}
/// <summary>Contains Friend Names</summary>
public class FriendNamesEventArgs : EventArgs
{
/// <summary>A dictionary where the Key is the ID of the Agent,
/// and the Value is a string containing their name</summary>
public Dictionary<UUID, string> Names { get; }
/// <summary>
/// Construct a new instance of the FriendNamesEventArgs class
/// </summary>
/// <param name="names">A dictionary where the Key is the ID of the Agent,
/// and the Value is a string containing their name</param>
public FriendNamesEventArgs(Dictionary<UUID, string> names)
{
this.Names = names;
}
}
/// <summary>Sent when another agent requests a friendship with our agent</summary>
public class FriendshipOfferedEventArgs : EventArgs
{
/// <summary>Get the ID of the agent requesting friendship</summary>
public UUID AgentID { get; }
/// <summary>Get the name of the agent requesting friendship</summary>
public string AgentName { get; }
/// <summary>Get the ID of the session, used in accepting or declining the
/// friendship offer</summary>
public UUID SessionID { get; }
/// <summary>
/// Construct a new instance of the FriendshipOfferedEventArgs class
/// </summary>
/// <param name="agentID">The ID of the agent requesting friendship</param>
/// <param name="agentName">The name of the agent requesting friendship</param>
/// <param name="imSessionID">The ID of the session, used in accepting or declining the
/// friendship offer</param>
public FriendshipOfferedEventArgs(UUID agentID, string agentName, UUID imSessionID)
{
this.AgentID = agentID;
this.AgentName = agentName;
this.SessionID = imSessionID;
}
}
/// <summary>A response containing the results of our request to form a friendship with another agent</summary>
public class FriendshipResponseEventArgs : EventArgs
{
/// <summary>Get the ID of the agent we requested a friendship with</summary>
public UUID AgentID { get; }
/// <summary>Get the name of the agent we requested a friendship with</summary>
public string AgentName { get; }
/// <summary>true if the agent accepted our friendship offer</summary>
public bool Accepted { get; }
/// <summary>
/// Construct a new instance of the FriendShipResponseEventArgs class
/// </summary>
/// <param name="agentID">The ID of the agent we requested a friendship with</param>
/// <param name="agentName">The name of the agent we requested a friendship with</param>
/// <param name="accepted">true if the agent accepted our friendship offer</param>
public FriendshipResponseEventArgs(UUID agentID, string agentName, bool accepted)
{
this.AgentID = agentID;
this.AgentName = agentName;
this.Accepted = accepted;
}
}
/// <summary>Contains data sent when a friend terminates a friendship with us</summary>
public class FriendshipTerminatedEventArgs : EventArgs
{
/// <summary>Get the ID of the agent that terminated the friendship with us</summary>
public UUID AgentID { get; }
/// <summary>Get the name of the agent that terminated the friendship with us</summary>
public string AgentName { get; }
/// <summary>
/// Construct a new instance of the FriendshipTerminatedEventArgs class
/// </summary>
/// <param name="agentID">The ID of the friend who terminated the friendship with us</param>
/// <param name="agentName">The name of the friend who terminated the friendship with us</param>
public FriendshipTerminatedEventArgs(UUID agentID, string agentName)
{
this.AgentID = agentID;
this.AgentName = agentName;
}
}
/// <summary>
/// Data sent in response to a <see cref="FindFriend"/> request which contains the information to allow us to map the friends location
/// </summary>
public class FriendFoundReplyEventArgs : EventArgs
{
/// <summary>Get the ID of the agent we have received location information for</summary>
public UUID AgentID { get; }
/// <summary>Get the region handle where our mapped friend is located</summary>
public ulong RegionHandle { get; }
/// <summary>Get the simulator local position where our friend is located</summary>
public Vector3 Location { get; }
/// <summary>
/// Construct a new instance of the FriendFoundReplyEventArgs class
/// </summary>
/// <param name="agentID">The ID of the agent we have requested location information for</param>
/// <param name="regionHandle">The region handle where our friend is located</param>
/// <param name="location">The simulator local position our friend is located</param>
public FriendFoundReplyEventArgs(UUID agentID, ulong regionHandle, Vector3 location)
{
this.AgentID = agentID;
this.RegionHandle = regionHandle;
this.Location = location;
}
}
#endregion
}