Group chat enhancements to combat SL bugs

This commit is contained in:
Casper Warden
2022-04-19 16:04:55 +01:00
parent 487907fb85
commit 2104e03b40
9 changed files with 237 additions and 82 deletions

View File

@@ -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)
});

View File

@@ -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

View File

@@ -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>();

View File

@@ -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, '');
}
}

View File

@@ -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);

View File

@@ -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) =>
{

View File

@@ -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);
});
}

View File

@@ -0,0 +1,6 @@
import { UUID } from '../classes/UUID';
export class GroupChatClosedEvent
{
groupID: UUID;
}

View File

@@ -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",