diff --git a/libsecondlife/AgentManager.cs b/libsecondlife/AgentManager.cs
index cfa055d8..36c62520 100644
--- a/libsecondlife/AgentManager.cs
+++ b/libsecondlife/AgentManager.cs
@@ -681,6 +681,15 @@ namespace libsecondlife
/// Simulator agent is now currently occupying
public delegate void RegionCrossedCallback(Simulator oldSim, Simulator newSim);
+ ///
+ /// Fired when group chat session confirmed joined
+ /// LLUUID of Session (groups UUID)
+ public delegate void GroupChatJoined(LLUUID groupChatSessionID);
+
+ /// Fired when agent group chat session terminated
+ /// LLUUID of Session (groups UUID)
+ public delegate void GroupChatLeft(LLUUID groupchatSessionID);
+
/// Callback for incoming chat packets
public event ChatCallback OnChat;
/// Callback for pop-up dialogs from scripts
@@ -705,6 +714,10 @@ namespace libsecondlife
public event MeanCollisionCallback OnMeanCollision;
/// Callback for the agent moving in to a neighboring sim
public event RegionCrossedCallback OnRegionCrossed;
+ /// Callback for when agent is confirmed joined group chat session.
+ public event GroupChatJoined OnGroupChatJoin;
+ /// Callback for when agent is confirmed to have left group chat session.
+ public event GroupChatLeft OnGroupChatLeft;
#endregion
@@ -885,7 +898,7 @@ namespace libsecondlife
private float health;
private int balance;
private LLUUID activeGroup;
-
+ private InternalDictionary> GroupChatSessions = new InternalDictionary>();
#endregion Private Members
///
@@ -933,7 +946,12 @@ namespace libsecondlife
Client.Network.RegisterCallback(PacketType.CrossedRegion, new NetworkManager.PacketCallback(CrossedRegionHandler));
// CAPS callbacks
Client.Network.RegisterEventCallback("EstablishAgentCommunication", new Caps.EventQueueCallback(EstablishAgentCommunicationEventHandler));
-
+ // Incoming Group Chat
+ Client.Network.RegisterEventCallback("ChatterBoxInvitation", new Caps.EventQueueCallback(ChatterBoxInvitationHandler));
+ // Outgoing Group Chat Reply
+ Client.Network.RegisterEventCallback("ChatterBoxSessionEventReply", new Caps.EventQueueCallback(ChatterBoxSessionEventHandler));
+ Client.Network.RegisterEventCallback("ChatterBoxSessionStartReply", new Caps.EventQueueCallback(ChatterBoxSessionStartReplyHandler));
+ Client.Network.RegisterEventCallback("ChatterBoxSessionAgentListUpdates", new Caps.EventQueueCallback(ChatterBoxSessionAgentListReplyHandler));
// Login
Client.Network.RegisterLoginResponseCallback(new NetworkManager.LoginResponseCallback(Network_OnLoginResponse));
}
@@ -1091,15 +1109,72 @@ namespace libsecondlife
/// Text message being sent
/// This does not appear to function with groups the agent is not in
public void InstantMessageGroup(string fromName, LLUUID groupUUID, string message)
+ {
+ lock (GroupChatSessions.Dictionary)
+ if (GroupChatSessions.ContainsKey(groupUUID))
+ {
+ ImprovedInstantMessagePacket im = new ImprovedInstantMessagePacket();
+
+ im.AgentData.AgentID = Client.Self.AgentID;
+ im.AgentData.SessionID = Client.Self.SessionID;
+ im.MessageBlock.Dialog = (byte)InstantMessageDialog.SessionSend;
+ im.MessageBlock.FromAgentName = Helpers.StringToField(fromName);
+ im.MessageBlock.FromGroup = false;
+ im.MessageBlock.Message = Helpers.StringToField(message);
+ im.MessageBlock.Offline = 0;
+ im.MessageBlock.ID = groupUUID;
+ im.MessageBlock.ToAgentID = groupUUID;
+ im.MessageBlock.Position = LLVector3.Zero;
+ im.MessageBlock.RegionID = LLUUID.Zero;
+ im.MessageBlock.BinaryBucket = Helpers.StringToField("\0");
+
+ Client.Network.SendPacket(im);
+ }
+ else
+ {
+ Client.Log("No Active group chat session appears to exist, use RequestJoinGroupChat() to join one",
+ Helpers.LogLevel.Error);
+ }
+ }
+
+ ///
+ /// Send a request to join a group chat session
+ ///
+ /// UUID of Group
+ public void RequestJoinGroupChat(LLUUID groupUUID)
{
ImprovedInstantMessagePacket im = new ImprovedInstantMessagePacket();
im.AgentData.AgentID = Client.Self.AgentID;
im.AgentData.SessionID = Client.Self.SessionID;
- im.MessageBlock.Dialog = (byte)InstantMessageDialog.SessionSend;
- im.MessageBlock.FromAgentName = Helpers.StringToField(fromName);
+ im.MessageBlock.Dialog = (byte)InstantMessageDialog.SessionGroupStart;
+ im.MessageBlock.FromAgentName = Helpers.StringToField(Client.Self.Name);
im.MessageBlock.FromGroup = false;
- im.MessageBlock.Message = Helpers.StringToField(message);
+ im.MessageBlock.Message = new byte[0];
+ im.MessageBlock.Offline = 0;
+ im.MessageBlock.ID = groupUUID;
+ im.MessageBlock.ToAgentID = groupUUID;
+ im.MessageBlock.BinaryBucket = new byte[0];
+ im.MessageBlock.Position = LLVector3.Zero;
+ im.MessageBlock.RegionID = LLUUID.Zero;
+
+ Client.Network.SendPacket(im);
+ }
+ ///
+ /// Request self terminates group chat. This will stop Group IM's from showing up
+ /// until session is rejoined or expires.
+ ///
+ /// UUID of Group
+ public void RequestLeaveGroupChat(LLUUID groupUUID)
+ {
+ ImprovedInstantMessagePacket im = new ImprovedInstantMessagePacket();
+
+ im.AgentData.AgentID = Client.Self.AgentID;
+ im.AgentData.SessionID = Client.Self.SessionID;
+ im.MessageBlock.Dialog = (byte)InstantMessageDialog.SessionDrop;
+ im.MessageBlock.FromAgentName = Helpers.StringToField(Client.Self.Name);
+ im.MessageBlock.FromGroup = false;
+ im.MessageBlock.Message = new byte[0];
im.MessageBlock.Offline = 0;
im.MessageBlock.ID = groupUUID;
im.MessageBlock.ToAgentID = groupUUID;
@@ -1107,7 +1182,6 @@ namespace libsecondlife
im.MessageBlock.Position = LLVector3.Zero;
im.MessageBlock.RegionID = LLUUID.Zero;
- // Send the message
Client.Network.SendPacket(im);
}
@@ -2197,6 +2271,7 @@ namespace libsecondlife
{
StructuredData.LLSDMap body = (StructuredData.LLSDMap)llsd;
+
if (Client.Settings.MULTIPLE_SIMS && body.ContainsKey("sim-ip-and-port"))
{
string ipAndPort = body["sim-ip-and-port"].AsString();
@@ -2465,6 +2540,141 @@ namespace libsecondlife
}
}
+ ///
+ /// Group Chat event handler
+ ///
+ ///
+ ///
+ ///
+ private void ChatterBoxSessionEventHandler(string capsKey, LLSD llsd, Simulator simulator)
+ {
+ // TODO: this appears to occur when you try and initiate group chat with an unopened session
+ //
+ // Key=ChatterBoxSessionEventReply
+ // llsd={
+ // ("error": "generic")
+ // ("event": "message")
+ // ("session_id": "3dafea18-cda1-9813-d5f1-fd3de6b13f8c") // group uuid
+ // ("success": "0")}
+ //LLSDMap map = (LLSDMap)llsd;
+ //LLUUID groupUUID = map["session_id"].AsUUID();
+ //Console.WriteLine("SessionEvent: Key={0} llsd={1}", capsKey, llsd.ToString());
+ }
+
+ ///
+ /// Response from request to join a group chat
+ ///
+ ///
+ ///
+ ///
+ private void ChatterBoxSessionStartReplyHandler(string capsKey, LLSD llsd, Simulator simulator)
+ {
+ LLSDMap map = (LLSDMap)llsd;
+ LLUUID sessionID = map["session_id"].AsUUID();
+
+ if (map["success"].AsBoolean())
+ {
+ LLSDArray agentlist = (LLSDArray)map["agents"];
+ List agents = new List();
+ foreach (LLSD id in agentlist)
+ agents.Add(id.AsUUID());
+
+ lock (GroupChatSessions.Dictionary)
+ {
+ if (GroupChatSessions.ContainsKey(sessionID))
+ GroupChatSessions.Dictionary[sessionID] = agents;
+ else
+ GroupChatSessions.Add(sessionID, agents);
+ }
+ }
+
+ if (OnGroupChatJoin != null)
+ {
+ try { OnGroupChatJoin(sessionID); }
+ catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); }
+ }
+ }
+
+ ///
+ /// Someone joined or left group chat
+ ///
+ ///
+ ///
+ ///
+ private void ChatterBoxSessionAgentListReplyHandler(string capsKey, LLSD llsd, Simulator simulator)
+ {
+ LLSDMap map = (LLSDMap)llsd;
+ LLUUID sessionID = map["session_id"].AsUUID();
+ LLSDMap update = (LLSDMap)map["updates"];
+ string errormsg = map["error"].AsString();
+
+ //if (errormsg.Equals("already in session"))
+ // return;
+
+ foreach (KeyValuePair kvp in update)
+ {
+ if (kvp.Value.Equals("ENTER"))
+ {
+ lock (GroupChatSessions.Dictionary)
+ {
+ if (!GroupChatSessions.Dictionary[sessionID].Contains((LLUUID)kvp.Key))
+ GroupChatSessions.Dictionary[sessionID].Add((LLUUID)kvp.Key);
+ }
+ }
+ else if (kvp.Value.Equals("LEAVE"))
+ {
+ lock (GroupChatSessions.Dictionary)
+ {
+ if (GroupChatSessions.Dictionary[sessionID].Contains((LLUUID)kvp.Key))
+ GroupChatSessions.Dictionary[sessionID].Remove((LLUUID)kvp.Key);
+
+ // we left session, remove from dictionary
+ if (kvp.Key.Equals(Client.Self.id) && OnGroupChatLeft != null)
+ {
+ GroupChatSessions.Dictionary.Remove(sessionID);
+ OnGroupChatLeft(sessionID);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Group Chat Request
+ ///
+ ///
+ ///
+ ///
+ private void ChatterBoxInvitationHandler(string capsKey, LLSD llsd, Simulator simulator)
+ {
+ if (OnInstantMessage != null)
+ {
+ LLSDMap map = (LLSDMap)llsd;
+ LLSDMap im = (LLSDMap)map["instantmessage"];
+ LLSDMap agent = (LLSDMap)im["agent_params"];
+ LLSDMap msg = (LLSDMap)im["message_params"];
+ LLSDMap msgdata = (LLSDMap)msg["data"];
+
+ InstantMessage message = new InstantMessage();
+
+ message.FromAgentID = map["from_id"].AsUUID();
+ message.FromAgentName = map["from_name"].AsString();
+ message.ToAgentID = msg["to_id"].AsString();
+ message.ParentEstateID = (uint)msg["parent_estate_id"].AsInteger();
+ message.RegionID = msg["region_id"].AsUUID();
+ message.Position.FromLLSD(msg["position"]);
+ message.Dialog = (InstantMessageDialog)msgdata["type"].AsInteger();
+ message.GroupIM = true;
+ message.IMSessionID = map["session_id"].AsUUID();
+ message.Timestamp = new DateTime(msgdata["timestamp"].AsInteger());
+ message.Message = msg["message"].AsString();
+ message.Offline = (InstantMessageOnline)msg["offline"].AsInteger();
+ message.BinaryBucket = msg["binary_bucket"].AsBinary();
+
+ try { OnInstantMessage(message, simulator); }
+ catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); }
+ }
+ }
#endregion Packet Handlers
}
}
diff --git a/libsecondlife/InternalDictionary.cs b/libsecondlife/InternalDictionary.cs
index f61df9a1..6b6dab7a 100644
--- a/libsecondlife/InternalDictionary.cs
+++ b/libsecondlife/InternalDictionary.cs
@@ -92,5 +92,37 @@ namespace libsecondlife
}
}
}
+
+ public bool ContainsKey(TKey key)
+ {
+ return Dictionary.ContainsKey(key);
+ }
+
+ public bool ContainsValue(TValue value)
+ {
+ return Dictionary.ContainsValue(value);
+ }
+
+ internal void Add(TKey key, TValue value)
+ {
+ Dictionary.Add(key, value);
+ }
+
+ internal bool Remove(TKey key)
+ {
+ return Dictionary.Remove(key);
+ }
+
+ internal void SafeAdd(TKey key, TValue value)
+ {
+ lock (Dictionary)
+ Dictionary.Add(key, value);
+ }
+
+ internal bool SafeRemove(TKey key)
+ {
+ lock (Dictionary)
+ return Dictionary.Remove(key);
+ }
}
}
diff --git a/libsecondlife/examples/TestClient/Commands/Communication/IMGroupCommand.cs b/libsecondlife/examples/TestClient/Commands/Communication/IMGroupCommand.cs
new file mode 100644
index 00000000..a2265c09
--- /dev/null
+++ b/libsecondlife/examples/TestClient/Commands/Communication/IMGroupCommand.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using libsecondlife;
+using libsecondlife.Packets;
+
+namespace libsecondlife.TestClient
+{
+ public class ImGroupCommand : Command
+ {
+ LLUUID ToGroupID = LLUUID.Zero;
+ ManualResetEvent WaitForSessionStart = new ManualResetEvent(false);
+ public ImGroupCommand(TestClient testClient)
+ {
+
+ Name = "imgroup";
+ Description = "Send an instant message to a group. Usage: imgroup [group_uuid] [message]";
+ }
+
+ public override string Execute(string[] args, LLUUID fromAgentID)
+ {
+ if (args.Length < 2)
+ return "Usage: imgroup [group_uuid] [message]";
+
+
+
+ if (LLUUID.TryParse(args[0], out ToGroupID))
+ {
+ string message = String.Empty;
+ for (int ct = 1; ct < args.Length; ct++)
+ message += args[ct] + " ";
+ message = message.TrimEnd();
+ if (message.Length > 1023) message = message.Remove(1023);
+
+ Client.Self.OnGroupChatJoin += new AgentManager.GroupChatJoined(Self_OnGroupChatJoin);
+ Client.Self.RequestJoinGroupChat(ToGroupID);
+ WaitForSessionStart.Reset();
+ if (WaitForSessionStart.WaitOne(10000, false))
+ {
+ Client.Self.InstantMessageGroup(ToGroupID, message);
+ }
+ else
+ {
+ return "Timeout waiting for group session start";
+ }
+ Client.Self.OnGroupChatJoin -= new AgentManager.GroupChatJoined(Self_OnGroupChatJoin);
+ Client.Self.RequestLeaveGroupChat(ToGroupID);
+ return "Instant Messaged group " + ToGroupID.ToString() + " with message: " + message;
+ }
+ else
+ {
+ return "failed to instant message group";
+ }
+ }
+
+ void Self_OnGroupChatJoin(LLUUID groupChatSessionID)
+ {
+ WaitForSessionStart.Set();
+ }
+ }
+}
diff --git a/libsecondlife/examples/TestClient/TestClient.cs b/libsecondlife/examples/TestClient/TestClient.cs
index d700dd02..747f6c8b 100644
--- a/libsecondlife/examples/TestClient/TestClient.cs
+++ b/libsecondlife/examples/TestClient/TestClient.cs
@@ -196,7 +196,7 @@ namespace libsecondlife.TestClient
if (im.FromAgentID != MasterKey)
{
// Received an IM from someone that is not the bot's master, ignore
- Console.WriteLine(" {1} (not master): {2} (@{3}:{4})", im.Dialog, im.FromAgentName, im.Message,
+ Console.WriteLine("<{0} ({1})> {2} (not master): {3} (@{4}:{5})", im.GroupIM ? "GroupIM" : "IM", im.Dialog, im.FromAgentName, im.Message,
im.RegionID, im.Position);
return;
}
@@ -204,13 +204,13 @@ namespace libsecondlife.TestClient
else if (GroupMembers != null && !GroupMembers.ContainsKey(im.FromAgentID))
{
// Received an IM from someone outside the bot's group, ignore
- Console.WriteLine(" {1} (not in group): {2} (@{3}:{4})", im.Dialog, im.FromAgentName,
+ Console.WriteLine("<{0} ({1})> {2} (not in group): {3} (@{4}:{5})", im.GroupIM ? "GroupIM" : "IM", im.Dialog, im.FromAgentName,
im.Message, im.RegionID, im.Position);
return;
}
// Received an IM from someone that is authenticated
- Console.WriteLine(" {1}: {2} (@{3}:{4})", im.Dialog, im.FromAgentName, im.Message, im.RegionID, im.Position);
+ Console.WriteLine("<{0} ({1})> {2}: {3} (@{4}:{5})", im.GroupIM ? "GroupIM" : "IM", im.Dialog, im.FromAgentName, im.Message, im.RegionID, im.Position);
if (im.Dialog == InstantMessageDialog.RequestTeleport)
{
diff --git a/libsecondlife/examples/TestClient/TestClient.csproj b/libsecondlife/examples/TestClient/TestClient.csproj
index 7b6bdfa3..ea036b6a 100644
--- a/libsecondlife/examples/TestClient/TestClient.csproj
+++ b/libsecondlife/examples/TestClient/TestClient.csproj
@@ -43,6 +43,7 @@
+