Group chat enhancements to combat SL bugs
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { GroupChatSessionJoinEvent, GroupChatEvent, UUID } from '../../lib';
|
||||
import { GroupChatClosedEvent } from '../../lib/events/GroupChatClosedEvent';
|
||||
import { ExampleBot } from '../ExampleBot';
|
||||
import { UUID } from '../../lib/classes/UUID';
|
||||
import { GroupChatEvent } from '../../lib/events/GroupChatEvent';
|
||||
|
||||
class GroupChat extends ExampleBot
|
||||
{
|
||||
private pings: {[key: string]: number} = {};
|
||||
private pings: {
|
||||
[key: string]: number
|
||||
} = {};
|
||||
|
||||
async onConnected(): Promise<void>
|
||||
{
|
||||
@@ -15,9 +17,6 @@ class GroupChat extends ExampleBot
|
||||
// Start a group chat session - equivalent to opening a group chat but not sending a message
|
||||
await this.bot.clientCommands.comms.startGroupChatSession(groupID, '');
|
||||
|
||||
// Send a group message
|
||||
await this.bot.clientCommands.comms.sendGroupMessage(groupID, 'Test');
|
||||
|
||||
const badGuyID = new UUID('1481561a-9113-46f8-9c02-9ac1bf005de7');
|
||||
await this.bot.clientCommands.comms.moderateGroupChat(groupID, badGuyID, true, true);
|
||||
|
||||
@@ -38,6 +37,23 @@ class GroupChat extends ExampleBot
|
||||
}
|
||||
});
|
||||
|
||||
this.bot.clientEvents.onGroupChatSessionJoin.subscribe((event: GroupChatSessionJoinEvent) =>
|
||||
{
|
||||
if (event.success)
|
||||
{
|
||||
console.log('We have joined a chat session! Group ID: ' + event.sessionID);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('We have FAILED to join a chat session! Group ID: ' + event.sessionID);
|
||||
}
|
||||
});
|
||||
|
||||
this.bot.clientEvents.onGroupChatClosed.subscribe((event: GroupChatClosedEvent) =>
|
||||
{
|
||||
console.log('Group chat session closed! Group ID: ' + event.groupID.toString());
|
||||
});
|
||||
|
||||
// Actually, maybe we want to ban the chump.
|
||||
await this.bot.clientCommands.group.banMembers(groupID, [badGuyID]);
|
||||
await this.bot.clientCommands.group.ejectFromGroup(groupID, badGuyID);
|
||||
@@ -61,6 +77,15 @@ class GroupChat extends ExampleBot
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
else if (event.message === '!rejoin')
|
||||
{
|
||||
console.log('Leaving the session..');
|
||||
await this.bot.clientCommands.comms.endGroupChatSession(event.groupID);
|
||||
console.log('Session terminated');
|
||||
console.log('Rejoining session');
|
||||
await this.bot.clientCommands.comms.startGroupChatSession(event.groupID, '');
|
||||
await this.bot.clientCommands.comms.sendGroupMessage(event.groupID, 'I am back!');
|
||||
}
|
||||
else if (event.from.toString() === this.bot.agentID().toString())
|
||||
{
|
||||
if (event.message.substr(0, 5) === 'ping ')
|
||||
@@ -78,4 +103,10 @@ class GroupChat extends ExampleBot
|
||||
}
|
||||
}
|
||||
|
||||
new GroupChat().run().then(() => {}).catch((err: Error) => { console.error(err) });
|
||||
new GroupChat().run().then(() =>
|
||||
{
|
||||
|
||||
}).catch((err: Error) =>
|
||||
{
|
||||
console.error(err)
|
||||
});
|
||||
|
||||
@@ -42,12 +42,13 @@ export class Agent
|
||||
regionAccess: string;
|
||||
agentAccess: string;
|
||||
currentRegion: Region;
|
||||
chatSessions: { [key: string]: {
|
||||
[key: string]: {
|
||||
hasVoice: boolean,
|
||||
isModerator: boolean
|
||||
}
|
||||
} } = {};
|
||||
chatSessions = new Map<string, {
|
||||
agents: Map<string, {
|
||||
hasVoice: boolean;
|
||||
isModerator: boolean
|
||||
}>,
|
||||
timeout?: Timer
|
||||
}>();
|
||||
controlFlags: ControlFlags = 0;
|
||||
openID: {
|
||||
'token'?: string,
|
||||
@@ -96,6 +97,8 @@ export class Agent
|
||||
private clientEvents: ClientEvents;
|
||||
private animSubscription?: Subscription;
|
||||
|
||||
public onGroupChatExpired = new Subject<UUID>();
|
||||
|
||||
constructor(clientEvents: ClientEvents)
|
||||
{
|
||||
this.inventory = new Inventory(clientEvents, this);
|
||||
@@ -103,27 +106,51 @@ export class Agent
|
||||
this.clientEvents.onGroupChatAgentListUpdate.subscribe((event: GroupChatSessionAgentListEvent) =>
|
||||
{
|
||||
const str = event.groupID.toString();
|
||||
if (this.chatSessions[str] === undefined)
|
||||
{
|
||||
this.chatSessions[str] = {};
|
||||
}
|
||||
|
||||
const agent = event.agentID.toString();
|
||||
|
||||
const session = this.chatSessions.get(str);
|
||||
if (session === undefined)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.entered)
|
||||
{
|
||||
this.chatSessions[str][agent] = {
|
||||
if (session.agents === undefined)
|
||||
{
|
||||
session.agents = new Map<string, {
|
||||
hasVoice: boolean;
|
||||
isModerator: boolean
|
||||
}>();
|
||||
}
|
||||
session.agents.set(agent, {
|
||||
hasVoice: event.canVoiceChat,
|
||||
isModerator: event.isModerator
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
delete this.chatSessions[str][agent];
|
||||
session.agents.delete(agent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateLastMessage(groupID: UUID): void
|
||||
{
|
||||
const str = groupID.toString();
|
||||
const entry = this.chatSessions.get(str);
|
||||
if (entry === undefined)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (entry.timeout !== undefined)
|
||||
{
|
||||
clearInterval(entry.timeout);
|
||||
entry.timeout = setTimeout(this.groupChatExpired.bind(this, groupID), 900000);
|
||||
}
|
||||
}
|
||||
|
||||
setIsEstateManager(is: boolean): void
|
||||
{
|
||||
this.estateManager = is;
|
||||
@@ -132,29 +159,54 @@ export class Agent
|
||||
getSessionAgentCount(uuid: UUID): number
|
||||
{
|
||||
const str = uuid.toString();
|
||||
if (this.chatSessions[str] === undefined)
|
||||
const session = this.chatSessions.get(str);
|
||||
if (session === undefined)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Object.keys(this.chatSessions[str]).length;
|
||||
return Object.keys(session.agents).length;
|
||||
}
|
||||
}
|
||||
|
||||
addChatSession(uuid: UUID): void
|
||||
addChatSession(uuid: UUID, timeout: boolean): boolean
|
||||
{
|
||||
const str = uuid.toString();
|
||||
if (this.chatSessions[str] === undefined)
|
||||
if (this.chatSessions.has(str))
|
||||
{
|
||||
this.chatSessions[str] = {};
|
||||
return false;
|
||||
}
|
||||
this.chatSessions.set(str, {
|
||||
agents: new Map<string, {
|
||||
hasVoice: boolean,
|
||||
isModerator: boolean
|
||||
}>(),
|
||||
timeout: timeout ? setTimeout(this.groupChatExpired.bind(this, uuid), 900000) : undefined
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private groupChatExpired(groupID: UUID): void
|
||||
{
|
||||
this.onGroupChatExpired.next(groupID);
|
||||
}
|
||||
|
||||
hasChatSession(uuid: UUID): boolean
|
||||
{
|
||||
const str = uuid.toString();
|
||||
return !(this.chatSessions[str] === undefined);
|
||||
return this.chatSessions.has(str);
|
||||
}
|
||||
|
||||
deleteChatSession(uuid: UUID): boolean
|
||||
{
|
||||
const str = uuid.toString();
|
||||
if (!this.chatSessions.has(str))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.chatSessions.delete(str);
|
||||
return true;
|
||||
}
|
||||
|
||||
setCurrentRegion(region: Region): void
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { GroupChatClosedEvent } from '../events/GroupChatClosedEvent';
|
||||
import { NewObjectEvent } from '../events/NewObjectEvent';
|
||||
import { ObjectUpdatedEvent } from '../events/ObjectUpdatedEvent';
|
||||
import { ObjectKilledEvent } from '../events/ObjectKilledEvent';
|
||||
@@ -43,6 +44,7 @@ export class ClientEvents
|
||||
onDisconnected: Subject<DisconnectEvent> = new Subject<DisconnectEvent>();
|
||||
onCircuitLatency: Subject<number> = new Subject<number>();
|
||||
onGroupChat: Subject<GroupChatEvent> = new Subject<GroupChatEvent>();
|
||||
onGroupChatClosed: Subject<GroupChatClosedEvent> = new Subject<GroupChatClosedEvent>();
|
||||
onGroupNotice: Subject<GroupNoticeEvent> = new Subject<GroupNoticeEvent>();
|
||||
onGroupChatSessionJoin: Subject<GroupChatSessionJoinEvent> = new Subject<GroupChatSessionJoinEvent>();
|
||||
onGroupChatAgentListUpdate: Subject<GroupChatSessionAgentListEvent> = new Subject<GroupChatSessionAgentListEvent>();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Subscription } from 'rxjs';
|
||||
import { GroupChatClosedEvent } from '../events/GroupChatClosedEvent';
|
||||
import { Agent } from './Agent';
|
||||
import { Circuit } from './Circuit';
|
||||
import { Packet } from './Packet';
|
||||
import { Message } from '../enums/Message';
|
||||
@@ -25,13 +28,11 @@ import { InventoryResponseEvent } from '../events/InventoryResponseEvent';
|
||||
|
||||
export class Comms
|
||||
{
|
||||
private circuit: Circuit;
|
||||
private clientEvents: ClientEvents;
|
||||
private groupChatExpiredSub?: Subscription;
|
||||
|
||||
constructor(circuit: Circuit, clientEvents: ClientEvents)
|
||||
constructor(public readonly circuit: Circuit, public readonly agent: Agent, public readonly clientEvents: ClientEvents)
|
||||
{
|
||||
this.clientEvents = clientEvents;
|
||||
this.circuit = circuit;
|
||||
this.groupChatExpiredSub = this.agent.onGroupChatExpired.subscribe(this.groupChatExpired.bind(this));
|
||||
|
||||
this.circuit.subscribeToMessages([
|
||||
Message.ImprovedInstantMessage,
|
||||
@@ -263,6 +264,14 @@ export class Comms
|
||||
this.clientEvents.onInstantMessage.next(imEvent);
|
||||
break;
|
||||
}
|
||||
case InstantMessageDialog.SessionDrop:
|
||||
{
|
||||
const groupChatClosedEvent = new GroupChatClosedEvent();
|
||||
groupChatClosedEvent.groupID = im.MessageBlock.ID;
|
||||
this.clientEvents.onGroupChatClosed.next(groupChatClosedEvent);
|
||||
this.agent.deleteChatSession(groupChatClosedEvent.groupID);
|
||||
break;
|
||||
}
|
||||
case InstantMessageDialog.SessionSend:
|
||||
{
|
||||
const groupChatEvent = new GroupChatEvent();
|
||||
@@ -270,6 +279,11 @@ export class Comms
|
||||
groupChatEvent.fromName = Utils.BufferToStringSimple(im.MessageBlock.FromAgentName);
|
||||
groupChatEvent.groupID = im.MessageBlock.ID;
|
||||
groupChatEvent.message = Utils.BufferToStringSimple(im.MessageBlock.Message);
|
||||
if (!this.agent.hasChatSession(groupChatEvent.groupID))
|
||||
{
|
||||
this.agent.addChatSession(groupChatEvent.groupID, true);
|
||||
}
|
||||
this.agent.updateLastMessage(groupChatEvent.groupID);
|
||||
this.clientEvents.onGroupChat.next(groupChatEvent);
|
||||
break;
|
||||
}
|
||||
@@ -335,6 +349,17 @@ export class Comms
|
||||
|
||||
shutdown(): void
|
||||
{
|
||||
if (this.groupChatExpiredSub !== undefined)
|
||||
{
|
||||
this.groupChatExpiredSub.unsubscribe();
|
||||
delete this.groupChatExpiredSub;
|
||||
}
|
||||
}
|
||||
|
||||
private async groupChatExpired(groupID: UUID): Promise<void>
|
||||
{
|
||||
// Reconnect to group chat since it's been idle for 15 minutes
|
||||
await this.agent.currentRegion.clientCommands.comms.endGroupChatSession(groupID, false);
|
||||
await this.agent.currentRegion.clientCommands.comms.startGroupChatSession(groupID, '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +337,11 @@ export class EventQueueClient
|
||||
if (gcsje.success)
|
||||
{
|
||||
gcsje.sessionID = new UUID(event['body']['session_id'].toString());
|
||||
this.agent.addChatSession(gcsje.sessionID);
|
||||
const added = this.agent.addChatSession(gcsje.sessionID, true);
|
||||
if (!added)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.clientEvents.onGroupChatSessionJoin.next(gcsje);
|
||||
}
|
||||
@@ -363,13 +367,13 @@ export class EventQueueClient
|
||||
};
|
||||
this.caps.capsPostXML('ChatSessionRequest', requested).then((_ignore: unknown) =>
|
||||
{
|
||||
this.agent.addChatSession(groupChatEvent.groupID);
|
||||
|
||||
this.agent.addChatSession(groupChatEvent.groupID, true);
|
||||
const gcsje = new GroupChatSessionJoinEvent();
|
||||
gcsje.sessionID = groupChatEvent.groupID;
|
||||
gcsje.success = true;
|
||||
this.clientEvents.onGroupChatSessionJoin.next(gcsje);
|
||||
this.clientEvents.onGroupChat.next(groupChatEvent);
|
||||
this.agent.updateLastMessage(groupChatEvent.groupID);
|
||||
}).catch((err) =>
|
||||
{
|
||||
console.error(err);
|
||||
|
||||
@@ -338,7 +338,7 @@ export class Region
|
||||
{
|
||||
this.objects = new ObjectStoreFull(this.circuit, agent, clientEvents, options);
|
||||
}
|
||||
this.comms = new Comms(this.circuit, clientEvents);
|
||||
this.comms = new Comms(this.circuit, agent, clientEvents);
|
||||
|
||||
this.parcelPropertiesSubscription = this.clientEvents.onParcelPropertiesEvent.subscribe(async(parcelProperties: ParcelPropertiesEvent) =>
|
||||
{
|
||||
|
||||
@@ -389,6 +389,48 @@ export class CommunicationsCommands extends CommandsBase
|
||||
});
|
||||
}
|
||||
|
||||
public async endGroupChatSession(groupID: UUID | string, removeEntry = true): Promise<void>
|
||||
{
|
||||
if (typeof groupID === 'string')
|
||||
{
|
||||
groupID = new UUID(groupID);
|
||||
}
|
||||
if (!this.agent.hasChatSession(groupID))
|
||||
{
|
||||
throw new Error('Group session does not exist');
|
||||
}
|
||||
const circuit = this.circuit;
|
||||
const agentName = this.agent.firstName + ' ' + this.agent.lastName;
|
||||
const im: ImprovedInstantMessageMessage = new ImprovedInstantMessageMessage();
|
||||
im.AgentData = {
|
||||
AgentID: this.agent.agentID,
|
||||
SessionID: circuit.sessionID
|
||||
};
|
||||
im.MessageBlock = {
|
||||
FromGroup: false,
|
||||
ToAgentID: groupID,
|
||||
ParentEstateID: 0,
|
||||
RegionID: UUID.zero(),
|
||||
Position: Vector3.getZero(),
|
||||
Offline: 0,
|
||||
Dialog: InstantMessageDialog.SessionDrop,
|
||||
ID: groupID,
|
||||
Timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
FromAgentName: Utils.StringToBuffer(agentName),
|
||||
Message: Buffer.allocUnsafe(0),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
if (removeEntry)
|
||||
{
|
||||
this.agent.deleteChatSession(groupID);
|
||||
}
|
||||
const sequenceNo = this.circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return this.circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
|
||||
startGroupChatSession(groupID: UUID | string, message: string): Promise<void>
|
||||
{
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
@@ -397,54 +439,47 @@ export class CommunicationsCommands extends CommandsBase
|
||||
{
|
||||
groupID = new UUID(groupID);
|
||||
}
|
||||
if (this.agent.hasChatSession(groupID))
|
||||
{
|
||||
resolve();
|
||||
}
|
||||
else
|
||||
{
|
||||
const circuit = this.circuit;
|
||||
const agentName = this.agent.firstName + ' ' + this.agent.lastName;
|
||||
const im: ImprovedInstantMessageMessage = new ImprovedInstantMessageMessage();
|
||||
im.AgentData = {
|
||||
AgentID: this.agent.agentID,
|
||||
SessionID: circuit.sessionID
|
||||
};
|
||||
im.MessageBlock = {
|
||||
FromGroup: false,
|
||||
ToAgentID: groupID,
|
||||
ParentEstateID: 0,
|
||||
RegionID: UUID.zero(),
|
||||
Position: Vector3.getZero(),
|
||||
Offline: 0,
|
||||
Dialog: InstantMessageDialog.SessionGroupStart,
|
||||
ID: groupID,
|
||||
Timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
FromAgentName: Utils.StringToBuffer(agentName),
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Utils.StringToBuffer('')
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const waitForJoin = this.currentRegion.clientEvents.onGroupChatSessionJoin.subscribe((event: GroupChatSessionJoinEvent) =>
|
||||
{
|
||||
if (event.sessionID.toString() === groupID.toString())
|
||||
{
|
||||
if (event.success)
|
||||
{
|
||||
waitForJoin.unsubscribe();
|
||||
|
||||
resolve();
|
||||
}
|
||||
else
|
||||
{
|
||||
reject();
|
||||
}
|
||||
const circuit = this.circuit;
|
||||
const agentName = this.agent.firstName + ' ' + this.agent.lastName;
|
||||
const im: ImprovedInstantMessageMessage = new ImprovedInstantMessageMessage();
|
||||
im.AgentData = {
|
||||
AgentID: this.agent.agentID,
|
||||
SessionID: circuit.sessionID
|
||||
};
|
||||
im.MessageBlock = {
|
||||
FromGroup: false,
|
||||
ToAgentID: groupID,
|
||||
ParentEstateID: 0,
|
||||
RegionID: UUID.zero(),
|
||||
Position: Vector3.getZero(),
|
||||
Offline: 0,
|
||||
Dialog: InstantMessageDialog.SessionGroupStart,
|
||||
ID: groupID,
|
||||
Timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
FromAgentName: Utils.StringToBuffer(agentName),
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Utils.StringToBuffer('')
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const waitForJoin = this.currentRegion.clientEvents.onGroupChatSessionJoin.subscribe((event: GroupChatSessionJoinEvent) =>
|
||||
{
|
||||
if (event.sessionID.toString() === groupID.toString())
|
||||
{
|
||||
if (event.success)
|
||||
{
|
||||
waitForJoin.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
}
|
||||
else
|
||||
{
|
||||
reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
6
lib/events/GroupChatClosedEvent.ts
Normal file
6
lib/events/GroupChatClosedEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { UUID } from '../classes/UUID';
|
||||
|
||||
export class GroupChatClosedEvent
|
||||
{
|
||||
groupID: UUID;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@caspertech/node-metaverse",
|
||||
"version": "0.5.31",
|
||||
"version": "0.5.32",
|
||||
"description": "A node.js interface for Second Life.",
|
||||
"main": "dist/lib/index.js",
|
||||
"types": "dist/lib/index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user