diff --git a/.gitignore b/.gitignore index 43309b4..44cbc56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /.idea /node_modules -/example/loginParameters.json -/example/npm-debug.log +/examples/loginParameters.json +/examples/npm-debug.log /caspertech-node-metaverse-*.tgz /npm-debug.log /dist diff --git a/example/testBot.js b/example/testBot.js deleted file mode 100644 index d5deeb1..0000000 --- a/example/testBot.js +++ /dev/null @@ -1,450 +0,0 @@ -// Here's a more modern example of how to use node-metaverse with async/await. -// Modern node.js required - -require('source-map-support').install(); - -const nmv = require('../dist/index'); -const loginParameters = new nmv.LoginParameters(); - -const parameters = require('./loginParameters.json'); -const uuid = require('uuid'); - -loginParameters.firstName = parameters.firstName; -loginParameters.lastName = parameters.lastName; -loginParameters.password = parameters.password; -loginParameters.start = parameters.start; - -//const options = nmv.BotOptionFlags.None; - -// If you don't intend to use the object store (i.e you have no interest in inworld objects, textures, etc, -// using nmv.BotOptionFlags.LiteObjectStore will drastically reduce the footprint and CPU usage. -// -// The full object store has a full searchable rtree index, the lite does not. -// -// For the minimum footprint, use : -// -// const options = nmv.BotOptionFlags.LiteObjectStore | nmv.BotOptionFlags.StoreMyAttachmentsOnly; - -const options = nmv.BotOptionFlags.None; - -const bot = new nmv.Bot(loginParameters, options); - -// This will tell the bot to keep trying to teleport back to the 'stay' location. -// You can specify a region and position, such as: -// bot.stayPut(true, 'Izanagi', new nmv.Vector3([128, 128, 21])); -// Note that the 'stay' location will be updated if you request or accept a lure (a teleport). -// If no region is specified, it will be set to the region you log in to. -bot.stayPut(true); - -let isConnected = false; - -const master = 'd1cd5b71-6209-4595-9bf0-771bf689ce00'; - -let loginResponse = null; - -bot.clientEvents.onLure.subscribe(async (lureEvent) => -{ - try - { - const regionInfo = await bot.clientCommands.grid.getRegionMapInfo(lureEvent.gridX / 256, lureEvent.gridY / 256); - if (lureEvent.from.toString() === master) - { - console.log('Accepting teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1)?'':'s') + ' present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage); - bot.clientCommands.teleport.acceptTeleport(lureEvent).then(() => {}).catch((err) => { - console.error('Teleport error:'); - console.error(err); - }); - } - else - { - console.log('Ignoring teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1)?'':'s') + ' present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage); - } - } - catch(error) - { - console.error("Failed to get region map info:"); - console.error(error); - } -}); - -bot.clientEvents.onInstantMessage.subscribe((IMEvent) => -{ - if (IMEvent.source === nmv.ChatSourceType.Agent) - { - if (!(IMEvent.flags & nmv.InstantMessageEventFlags.startTyping || IMEvent.flags & nmv.InstantMessageEventFlags.finishTyping)) - { - bot.clientCommands.comms.typeInstantMessage(IMEvent.from, 'Thanks for the message! This account is a scripted agent (bot), so cannot reply to your query. Sorry!').then(() => {}); - } - } -}); - -bot.clientEvents.onFriendRequest.subscribe((event) => -{ - if (event.from.toString() === master) - { - console.log("Accepting friend request from " + event.fromName); - bot.clientCommands.friends.acceptFriendRequest(event).then(() => {}); - } - else - { - console.log("Rejecting friend request from " + event.fromName); - bot.clientCommands.friends.rejectFriendRequest(event).then(() => {}); - } -}); - -bot.clientEvents.onInventoryOffered.subscribe((event) => -{ - if (event.from.toString() === master) - { - console.log("Accepting inventory offer from " + event.fromName); - bot.clientCommands.inventory.acceptInventoryOffer(event).then(() => {}); - } - else - { - console.log("Rejecting inventory offer from " + event.fromName); - bot.clientCommands.inventory.rejectInventoryOffer(event).then(() => {}); - } -}); - -bot.clientEvents.onDisconnected.subscribe((DisconnectEvent) => -{ - isConnected = false; - console.log("Disconnected from simulator: "+DisconnectEvent.message); - if (!DisconnectEvent.requested) - { - setTimeout(() => - { - console.log("Reconnecting"); - connect().then(() => {}); - }, 5000) - } -}); - -let pings = {}; - -bot.clientEvents.onGroupChat.subscribe(async (GroupChatEvent) => -{ - console.log("Group chat: " + GroupChatEvent.fromName + ': ' + GroupChatEvent.message); - if (GroupChatEvent.message === '!ping') - { - let ping = uuid.v4(); - pings[ping] = Math.floor(new Date().getTime()); - try - { - const memberCount = await bot.clientCommands.comms.sendGroupMessage(GroupChatEvent.groupID, 'ping '+ping); - console.log('Group message sent to ' + memberCount + ' members'); - } - catch(error) - { - console.error('Failed to send group message:'); - console.error(error); - } - } - else if (GroupChatEvent.from.toString() === loginResponse.agent.agentID.toString()) - { - if (GroupChatEvent.message.substr(0, 5) === 'ping ') - { - const pingID = GroupChatEvent.message.substr(5); - if (pings[pingID]) - { - console.log("found ping"); - const time = (new Date().getTime()) - pings[pingID]; - delete pings[pingID]; - bot.clientCommands.comms.sendGroupMessage(GroupChatEvent.groupID, 'Chat lag: ' + time + 'ms').then(() => {}); - } - else - { - console.log("ping not found |"+pingID+"|"); - } - } - - } -}); - -bot.clientEvents.onGroupNotice.subscribe(async(GroupNoticeEvent) => -{ - // Get group name - const groupProfile = await bot.clientCommands.group.getGroupProfile(GroupNoticeEvent.groupID); - - console.log('Group notice from ' + GroupNoticeEvent.fromName + ' (' + GroupNoticeEvent.from + '), from group ' + groupProfile.Name + ' (' + GroupNoticeEvent.groupID + ')'); - console.log('Subject: ' + GroupNoticeEvent.subject); - console.log('Message: ' + GroupNoticeEvent.message); -}); - -bot.clientEvents.onGroupInvite.subscribe(async (GroupInviteEvent) => -{ - - console.log('Group invite from ' + GroupInviteEvent.fromName + ': '+GroupInviteEvent.message); - - //Resolve avatar key - try - { - const key = await bot.clientCommands.grid.avatarName2Key(GroupInviteEvent.fromName); - if (key.toString() === master) - { - console.log('Accepting'); - bot.clientCommands.group.acceptGroupInvite(GroupInviteEvent).then(() => {}); - } - else - { - console.log('Unauthorised - rejecting'); - bot.clientCommands.group.rejectGroupInvite(GroupInviteEvent).then(() => {}); - } - } - catch(error) - { - console.error('Failed to respond to group invite:'); - console.error(error); - } -}); - -bot.clientEvents.onFriendResponse.subscribe((response) => -{ - if (response.accepted) - { - console.log(response.fromName + ' accepted your friend request'); - } - else - { - console.log(response.fromName + ' declined your friend request'); - } -}); - -async function connect() -{ - try - { - console.log("Logging in.."); - loginResponse = await bot.login(); - - console.log("Login complete"); - - //Establish circuit with region - await bot.connectToSim(); - - console.log("Connected to simulator"); - isConnected = true; - // Do some stuff - //bot.clientCommands.comms.typeLocalMessage('Never fear, I am here!', 2000); - //bot.clientCommands.group.sendGroupNotice('503e8ef6-e119-ff5e-2524-24f290dd3867', 'Test', 'testy testy test'); - - // Group invite example - // Just omit the role parameter for "everyone" role - // - // bot.clientCommands.group.sendGroupInvite("c6424e05-6e2c-fb03-220b-ca7904d11e04", "d1cd5b71-6209-4595-9bf0-771bf689ce00"); - - // Advanced group invite example - // - // Retrieve group roles - - const userToInvite = new nmv.UUID("d1cd5b71-6209-4595-9bf0-771bf689ce00"); - const groupID = new nmv.UUID("c6424e05-6e2c-fb03-220b-ca7904d11e04"); - - // If you want to wait here for the request to be acknowledged, you can add "await" - bot.clientCommands.friends.sendFriendRequest(master, 'Be friends with me?').then(() => {}); - - const folders = bot.clientCommands.inventory.getInventoryRoot().getChildFolders(); - for (const folder of folders) - { - console.log('Top level folder: ' + folder.name); - folder.populate().then(() => {}); - } - - const roles = await bot.clientCommands.group.getGroupRoles(groupID); - - for(const role of roles) - { - if (role.Name === 'Officers') - { - // IMPORTANT: IN PRODUCTION, IT IS HIGHLY RECOMMENDED TO CACHE THIS LIST. - // - try - { - const members = await bot.clientCommands.group.getMemberList(groupID); - let found = true; - for (const member of members) - { - if (member.AgentID.toString() === userToInvite.toString()) - { - found = true; - } - } - if (found) - { - console.log("User already in group, skipping invite"); - } - else - { - bot.clientCommands.group.sendGroupInvite(groupID, userToInvite, role.RoleID).then(() => {}); - } - } - catch(error) - { - console.error('Error retrieving member list for group invite'); - } - } - } - - await bot.waitForEventQueue(); - try - { - // Get map location of Casper Warden (should (hopefully)! fail if you don't have map rights on me.. - const regionLocation = await bot.clientCommands.friends.getFriendMapLocation('d1cd5b71-6209-4595-9bf0-771bf689ce00'); - console.log('Casper is in ' + regionLocation.regionName + ' at <' + regionLocation.localX + ', ' + regionLocation.localY + '> and there are ' + regionLocation.avatars.length + ' other avatars there too! You stalker!'); - - } - catch(error) - { - console.log('Map location request failed. You probably do not have map rights on Casper, or he is offline.'); - } - - // By default, camera view distance is set to 1, to minimise memory and bandwidth consumption. - // This algorithm will slowly drag the camera up into the sky, scanning for objects. - let height = 64; - let lastObjects = 0; - - bot.clientCommands.agent.setCamera( - new nmv.Vector3([128, 128, height]), - new nmv.Vector3([128, 128, 0]), - 256, - new nmv.Vector3([-1.0, 0, 0]), - new nmv.Vector3([0.0, 1.0, 0])); - - - // Get group member list - try - { - const memberList = await bot.clientCommands.group.getMemberList('f0466f71-abf4-1559-db93-50352d13ae74'); - } - catch (error) - { - // Probably access denied - console.error(error); - } - - // Get group ban list - try - { - const banList = await bot.clientCommands.group.getBanList('f0466f71-abf4-1559-db93-50352d13ae74'); - } - catch (error) - { - // Probably access denied - console.error(error); - } - - - // Moderate group member - try - { - const groupKey = '4b35083d-b51a-a148-c400-6f1038a5589e'; - const avatarKey = '4300b952-d20e-4aa5-b3d6-d2e4d675880d'; - - // Note this will start a new group chat session if one does not already exist - await bot.clientCommands.comms.moderateGroupChat(groupKey, avatarKey, true, true); - - // Now, the group mute stuff is often pretty useless because an avatar can just leave the session and re-join. - // Let's enforce it a little better. - const groupChatSubscriber = bot.clientEvents.onGroupChatAgentListUpdate.subscribe((event) => - { - if (event.groupID.toString() === groupKey && event.agentID.toString() === avatarKey && event.entered) - { - bot.clientCommands.comms.moderateGroupChat(groupKey, avatarKey, true, true).then(() => - { - console.log('Re-enforced mute on ' + avatarKey); - }).catch((err) => - { - console.error(err); - }); - } - }); - - // Send a group message - await bot.clientCommands.comms.sendGroupMessage(groupKey, 'Test'); - - // "Open" group chat but don't send a message - await bot.clientCommands.comms.startGroupChatSession("f0466f71-abf4-1559-db93-50352d13ae74", ""); - } - catch (error) - { - console.error(error); - } - - //await bot.clientCommands.friends.grantFriendRights('d1cd5b71-6209-4595-9bf0-771bf689ce00', nmv.RightsFlags.CanModifyObjects | nmv.RightsFlags.CanSeeOnline | nmv.RightsFlags.CanSeeOnMap ); - - const parcelInMiddle = await bot.clientCommands.region.getParcelAt(128, 128); - console.log('Parcel at 128x128 is ' + parcelInMiddle.Name); - - const parcels = await bot.clientCommands.region.getParcels(); - console.log('Parcels on region:'); - console.log('========================'); - for (const p of parcels) - { - console.log(p.Name); - } - console.log('========================'); - - await bot.clientCommands.comms.sendTeleport('d1cd5b71-6209-4595-9bf0-771bf689ce00'); - } - catch (error) - { - isConnected = false; - console.log("Error:"); - console.error(error); - setTimeout(() => - { - connect().then(() => {}); - }, 5000) - } -} - -try -{ - connect().then(() => {}); -} -catch(error) -{ - console.error('Connection failure: '); - console.log(error); -} - - -async function exitHandler(options, err) -{ - if (err) - { - console.log(err.stack); - } - if (isConnected) - { - console.log("Disconnecting"); - try - { - await bot.close(); - } - catch(error) - { - console.error('Error when closing client:'); - console.error(error); - } - process.exit(); - return; - } - if (options.exit) - { - process.exit(); - } -} - -//do something when app is closing -process.on('exit', exitHandler.bind(null,{})); - -//catches ctrl+c event -process.on('SIGINT', exitHandler.bind(null, {exit:true})); - -// catches "kill pid" -process.on('SIGUSR1', exitHandler.bind(null, {exit:true})); -process.on('SIGUSR2', exitHandler.bind(null, {exit:true})); - -//catches uncaught exceptions -process.on('uncaughtException', exitHandler.bind(null, {exit:true})); diff --git a/examples/Camera/Camera.ts b/examples/Camera/Camera.ts new file mode 100644 index 0000000..836c281 --- /dev/null +++ b/examples/Camera/Camera.ts @@ -0,0 +1,18 @@ +import { ExampleBot } from '../ExampleBot'; +import { Vector3 } from '../../lib/classes/Vector3'; + +class Camera extends ExampleBot +{ + async onConnected() + { + const height = 64; + this.bot.clientCommands.agent.setCamera( + new Vector3([128, 128, height]), + new Vector3([128, 128, 0]), + 256, + new Vector3([-1.0, 0, 0]), + new Vector3([0.0, 1.0, 0])); + } +} + +new Camera().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/examples/ExampleBot.ts b/examples/ExampleBot.ts new file mode 100644 index 0000000..a2c8cab --- /dev/null +++ b/examples/ExampleBot.ts @@ -0,0 +1,171 @@ +import Signals = NodeJS.Signals; +import Timeout = NodeJS.Timeout; + +import * as path from 'path'; +import { LoginResponse } from '../lib/classes/LoginResponse'; +import { Bot } from '../lib/Bot'; +import { LoginParameters } from '../lib/classes/LoginParameters'; +import { BotOptionFlags } from '../lib/enums/BotOptionFlags'; + +export class ExampleBot +{ + protected masterAvatar = 'd1cd5b71-6209-4595-9bf0-771bf689ce00'; + protected isConnected = false; + protected isConnecting = false; + protected loginResponse?: LoginResponse; + + protected bot: Bot; + private reconnectTimer?: Timeout; + + constructor() + { + const loginParameters = new LoginParameters(); + const parameters = require(path.join(__dirname, '..', '..', 'examples', 'loginParameters.json')); + loginParameters.firstName = parameters.firstName; + loginParameters.lastName = parameters.lastName; + loginParameters.password = parameters.password; + loginParameters.start = parameters.start; + + // If you don't intend to use the object store (i.e you have no interest in inworld objects, textures, etc, + // using nmv.BotOptionFlags.LiteObjectStore will drastically reduce the footprint and CPU usage. + // + // The full object store has a full searchable rtree index, the lite does not. + // + // For the minimum footprint, use : + // + // const options = nmv.BotOptionFlags.LiteObjectStore | nmv.BotOptionFlags.StoreMyAttachmentsOnly; + + const options = BotOptionFlags.None; + + this.bot = new Bot(loginParameters, options); + + // This will tell the bot to keep trying to teleport back to the 'stay' location. + // You can specify a region and position, such as: + // bot.stayPut(true, 'Izanagi', new nmv.Vector3([128, 128, 21])); + // Note that the 'stay' location will be updated if you request or accept a lure (a teleport). + // If no region is specified, it will be set to the region you log in to. + this.bot.stayPut(true); + } + + public async run() + { + const exitHandler = async(options: { exit?: boolean }, err: Error | number | Signals) => + { + if (err && err instanceof Error) + { + console.log(err.stack); + } + if (this.isConnected) + { + console.log('Disconnecting'); + try + { + await this.bot.close(); + } + catch (error) + { + console.error('Error when closing client:'); + console.error(error); + } + process.exit(); + return; + } + if (options.exit) + { + process.exit(); + } + } + + + + // Do something when app is closing + process.on('exit', exitHandler.bind(this, {})); + + // Catches ctrl+c event + process.on('SIGINT', exitHandler.bind(this, { exit: true })); + + // Catches "kill pid" + process.on('SIGUSR1', exitHandler.bind(this, { exit: true })); + process.on('SIGUSR2', exitHandler.bind(this, { exit: true })); + + // Catches uncaught exceptions + process.on('uncaughtException', exitHandler.bind(this, { exit: true })); + + await this.login(); + } + + protected async onConnected() + { + + } + + private async login() + { + if (this.isConnecting) + { + return; + } + this.isConnecting = true; + try + { + if (this.reconnectTimer !== undefined) + { + clearInterval(this.reconnectTimer); + } + this.reconnectTimer = setInterval(this.reconnectCheck.bind(this), 60000); + + console.log('Logging in..'); + this.loginResponse = await this.bot.login(); + + console.log('Login complete'); + + // Establish circuit with region + await this.bot.connectToSim(); + + console.log('Waiting for event queue'); + await this.bot.waitForEventQueue(); + + this.isConnected = true; + } + finally + { + this.isConnecting = false; + } + return this.connected(); + } + + private async reconnectCheck() + { + if (!this.isConnected) + { + await this.login(); + } + } + + private async connected() + { + this.bot.clientEvents.onDisconnected.subscribe((event) => + { + if (event.requested) + { + if (this.reconnectTimer !== undefined) + { + clearInterval(this.reconnectTimer); + } + } + this.isConnected = false; + console.log('Disconnected from simulator: ' + event.message); + }); + await this.onConnected(); + } + + private async close() + { + if (this.reconnectTimer !== undefined) + { + clearInterval(this.reconnectTimer); + this.reconnectTimer = undefined; + } + return this.bot.close(); + } +} diff --git a/examples/Friends/Friends.ts b/examples/Friends/Friends.ts new file mode 100644 index 0000000..9d29e2b --- /dev/null +++ b/examples/Friends/Friends.ts @@ -0,0 +1,58 @@ +import { ExampleBot } from '../ExampleBot'; +import { FriendRequestEvent } from '../../lib/events/FriendRequestEvent'; +import { FriendResponseEvent } from '../../lib/events/FriendResponseEvent'; + +class Friends extends ExampleBot +{ + async onConnected() + { + this.bot.clientEvents.onFriendRequest.subscribe(this.onFriendRequest.bind(this)); + this.bot.clientEvents.onFriendResponse.subscribe(this.onFriendResponse.bind(this)); + + this.bot.clientCommands.friends.sendFriendRequest(this.masterAvatar, 'Be friends with me?').then(() => {}); + + try + { + // Get map location of the master avatar. Will fail if you don't have map rights + const regionLocation = await this.bot.clientCommands.friends.getFriendMapLocation(this.masterAvatar); + console.log('Master is in ' + regionLocation.regionName + ' at <' + regionLocation.localX + ', ' + regionLocation.localY + '> and there are ' + regionLocation.avatars.length + ' other avatars there too! You stalker!'); + } + catch (error) + { + console.log('Map location request failed. The bot probably does not have map rights on the master avatar, or they are offline.'); + } + } + + async onFriendRequest(event: FriendRequestEvent) + { + if (event.from.toString() === this.masterAvatar) + { + console.log('Accepting friend request from ' + event.fromName); + this.bot.clientCommands.friends.acceptFriendRequest(event).then(() => + { + }); + } + else + { + console.log('Rejecting friend request from ' + event.fromName); + this.bot.clientCommands.friends.rejectFriendRequest(event).then(() => + { + }); + } + } + + async onFriendResponse(response: FriendResponseEvent) + { + if (response.accepted) + { + console.log(response.fromName + ' accepted your friend request'); + } + else + { + console.log(response.fromName + ' declined your friend request'); + } + } + +} + +new Friends().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/examples/Groups/Group.ts b/examples/Groups/Group.ts new file mode 100644 index 0000000..06fe60c --- /dev/null +++ b/examples/Groups/Group.ts @@ -0,0 +1,96 @@ +import { ExampleBot } from '../ExampleBot'; +import { UUID } from '../../lib/classes/UUID'; +import { GroupNoticeEvent } from '../../lib/events/GroupNoticeEvent'; + +class Group extends ExampleBot +{ + async onConnected() + { + this.bot.clientEvents.onGroupNotice.subscribe(this.onGroupNotice.bind(this)); + + // Group invite example + // Just omit the role parameter for "everyone" role + // + // bot.clientCommands.group.sendGroupInvite("c6424e05-6e2c-fb03-220b-ca7904d11e04", "d1cd5b71-6209-4595-9bf0-771bf689ce00"); + + // Advanced group invite example + // + + const userToInvite = new UUID('d1cd5b71-6209-4595-9bf0-771bf689ce00'); + const groupID = new UUID('4b35083d-b51a-a148-c400-6f1038a5589e'); + + // Retrieve group roles + const roles = await this.bot.clientCommands.group.getGroupRoles(groupID); + + for (const role of roles) + { + if (role.Name === 'Officers') + { + // IMPORTANT: IN PRODUCTION, IT IS HIGHLY RECOMMENDED TO CACHE THIS LIST. + // + try + { + const members = await this.bot.clientCommands.group.getMemberList(groupID); + let found = true; + for (const member of members) + { + if (member.AgentID.toString() === userToInvite.toString()) + { + found = true; + } + } + if (found) + { + console.log('User already in group, skipping invite'); + } + else + { + this.bot.clientCommands.group.sendGroupInvite(groupID, userToInvite, role.RoleID).then(() => + { + }); + } + } + catch (error) + { + console.error('Error retrieving member list for group invite'); + } + } + } + + // Get group member list + try + { + const memberList = await this.bot.clientCommands.group.getMemberList(groupID); + console.log(memberList.length + ' members in member list'); + } + catch (error) + { + // Probably access denied + console.error(error); + } + + // Get group ban list + try + { + const banList = await this.bot.clientCommands.group.getBanList(groupID); + console.log(banList.length + ' members in ban list'); + } + catch (error) + { + // Probably access denied + console.error(error); + } + } + + async onGroupNotice(event: GroupNoticeEvent) + { + // Get group name + const groupProfile = await this.bot.clientCommands.group.getGroupProfile(event.groupID); + + console.log('Group notice from ' + event.fromName + ' (' + event.from + '), from group ' + groupProfile.Name + ' (' + event.groupID + ')'); + console.log('Subject: ' + event.subject); + console.log('Message: ' + event.message); + } +} + +new Group().run().then(() => {}).catch((err: Error) => { console.error(err) }); diff --git a/examples/Groups/GroupChat.ts b/examples/Groups/GroupChat.ts new file mode 100644 index 0000000..be2b734 --- /dev/null +++ b/examples/Groups/GroupChat.ts @@ -0,0 +1,80 @@ +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} = {}; + + async onConnected() + { + const groupID = new UUID('4b35083d-b51a-a148-c400-6f1038a5589e'); + + this.bot.clientEvents.onGroupChat.subscribe(this.onGroupChat.bind(this)); + + // 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); + + // Now, the group mute stuff is often pretty useless because an avatar can just leave the session and re-join. + // Let's enforce it a little better. + const groupChatSubscriber = this.bot.clientEvents.onGroupChatAgentListUpdate.subscribe((event) => + { + if (event.groupID.equals(groupID) && event.agentID.equals(badGuyID) && event.entered) + { + this.bot.clientCommands.comms.moderateGroupChat(groupID, badGuyID, true, true).then(() => + { + console.log('Re-enforced mute on ' + badGuyID.toString()); + }).catch((err) => + { + console.error(err); + }); + } + }); + + // Actually, maybe we want to ban the chump. + await this.bot.clientCommands.group.banMembers(groupID, [badGuyID]); + await this.bot.clientCommands.group.ejectFromGroup(groupID, badGuyID); + } + + async onGroupChat(event: GroupChatEvent) + { + console.log('Group chat: ' + event.fromName + ': ' + event.message); + if (event.message === '!ping') + { + const ping = UUID.random().toString(); + this.pings[ping] = Math.floor(new Date().getTime()); + try + { + const memberCount = await this.bot.clientCommands.comms.sendGroupMessage(event.groupID, 'ping ' + ping); + console.log('Group message sent to ' + memberCount + ' members'); + } + catch (error) + { + console.error('Failed to send group message:'); + console.error(error); + } + } + else if (event.from.toString() === this.bot.agentID().toString()) + { + if (event.message.substr(0, 5) === 'ping ') + { + const pingID = event.message.substr(5); + if (this.pings[pingID]) + { + const time = (new Date().getTime()) - this.pings[pingID]; + delete this.pings[pingID]; + await this.bot.clientCommands.comms.sendGroupMessage(event.groupID, 'Chat lag: ' + time + 'ms'); + } + } + + } + } +} + +new GroupChat().run().then(() => {}).catch((err: Error) => { console.error(err) }); diff --git a/examples/InstantMessages/InstantMessages.ts b/examples/InstantMessages/InstantMessages.ts new file mode 100644 index 0000000..66306b6 --- /dev/null +++ b/examples/InstantMessages/InstantMessages.ts @@ -0,0 +1,35 @@ + +import { ExampleBot } from '../ExampleBot'; +import { InstantMessageEvent } from '../../lib/events/InstantMessageEvent'; +import { ChatSourceType } from '../../lib/enums/ChatSourceType'; +import { InstantMessageEventFlags } from '../../lib/enums/InstantMessageEventFlags'; + +class InstantMessages extends ExampleBot +{ + constructor() + { + super(); + } + + async onConnected() + { + this.bot.clientEvents.onInstantMessage.subscribe(this.onInstantMessage.bind(this)); + } + + async onInstantMessage(event: InstantMessageEvent) + { + if (event.source === ChatSourceType.Agent) + { + if (!(event.flags & InstantMessageEventFlags.startTyping || event.flags & InstantMessageEventFlags.finishTyping)) + { + // typeInstantMessage will emulate a human-ish typing speed + await this.bot.clientCommands.comms.typeInstantMessage(event.from, 'Thanks for the message! This account is a scripted agent (bot), so cannot reply to your query. Sorry!'); + + // sendInstantMessage will send it instantly + await this.bot.clientCommands.comms.sendInstantMessage(event.from, 'Of course I still love you!'); + } + } + } +} + +new InstantMessages().run().then(() => {}).catch((err: Error) => { console.error(err) }); diff --git a/examples/Inventory/Inventory.ts b/examples/Inventory/Inventory.ts new file mode 100644 index 0000000..cfb4ee9 --- /dev/null +++ b/examples/Inventory/Inventory.ts @@ -0,0 +1,134 @@ +import { ExampleBot } from '../ExampleBot'; +import { InventoryFolder } from '../../lib/classes/InventoryFolder'; +import { FolderType } from '../../lib/enums/FolderType'; +import { InventoryItem } from '../../lib/classes/InventoryItem'; +import { LLLindenText } from '../../lib/classes/LLLindenText'; +import { AssetType } from '../../lib/enums/AssetType'; +import { InventoryType } from '../../lib/enums/InventoryType'; +import { PermissionMask } from '../../lib/enums/PermissionMask'; +import { InventoryResponseEvent } from '../../lib/events/InventoryResponseEvent'; +import { InventoryOfferedEvent } from '../../lib/events/InventoryOfferedEvent'; + +class Inventory extends ExampleBot +{ + async onConnected() + { + this.bot.clientEvents.onInventoryOffered.subscribe(this.onInventoryOffered.bind(this)); + this.bot.clientEvents.onInventoryResponse.subscribe(this.onInventoryResponse.bind(this)); + + // Get the root inventory folder + const rootFolder = this.bot.clientCommands.inventory.getInventoryRoot(); + + // Populate the root folder + await rootFolder.populate(false); + + const exampleFolderName = 'node-metaverse example'; + const exampleNotecardName = 'Example Notecard'; + + let exampleFolder: InventoryFolder | undefined = undefined; + for (const childFolder of rootFolder.folders) + { + if (childFolder.name === exampleFolderName) + { + exampleFolder = childFolder; + await exampleFolder.populate(false); + break; + } + } + + // Our folder doesnt' seem to exist, so create it + if (exampleFolder === undefined) + { + exampleFolder = await rootFolder.createFolder(exampleFolderName, FolderType.None); + } + + // See if we've already made our test notecard to avoid clutter.. + let exampleNotecard: InventoryItem | undefined = undefined; + for (const childItem of exampleFolder.items) + { + if (childItem.name === exampleNotecardName) + { + exampleNotecard = childItem; + break; + } + } + + // Create the notecard + if (exampleNotecard === undefined) + { + const notecard = new LLLindenText(); + notecard.body = 'This is a notecard I made all by myself at ' + new Date().toString(); + exampleNotecard = await exampleFolder.uploadAsset(AssetType.Notecard, InventoryType.Notecard, notecard.toAsset(), exampleNotecardName, 'This is an example notecard'); + } + + // Set notecard to transfer only + + exampleNotecard.permissions.nextOwnerMask = PermissionMask.Transfer | PermissionMask.Modify; + await exampleNotecard.update(); + + // Give the notecard to our owner + await this.bot.clientCommands.comms.giveInventory(this.masterAvatar, exampleNotecard); + + // Enumerate library + const folders = this.bot.clientCommands.inventory.getLibraryRoot().getChildFolders(); + for (const folder of folders) + { + await this.iterateFolder(folder, '[ROOT]'); + } + console.log('Done iterating through library'); + } + + async onInventoryResponse(response: InventoryResponseEvent) + { + if (response.accepted) + { + console.log(response.fromName + ' accepted your inventory offer'); + } + else + { + console.log(response.fromName + ' declined your inventory offer'); + } + } + + async iterateFolder(folder: InventoryFolder, prefix: string) + { + console.log(prefix + ' [' + folder.name + ']'); + await folder.populate(false); + + for (const subFolder of folder.folders) + { + await this.iterateFolder(subFolder, prefix + ' [' + folder.name + ']'); + } + + for (const item of folder.items) + { + console.log(prefix + ' [' + folder.name + ']' + ': ' + item.name); + + if (item.name === 'anim SMOOTH') + { + // Send this to our master av + + } + } + } + + async onInventoryOffered(event: InventoryOfferedEvent) + { + if (event.from.toString() === this.masterAvatar) + { + console.log('Accepting inventory offer from ' + event.fromName); + this.bot.clientCommands.inventory.acceptInventoryOffer(event).then(() => + { + }); + } + else + { + console.log('Rejecting inventory offer from ' + event.fromName); + this.bot.clientCommands.inventory.rejectInventoryOffer(event).then(() => + { + }); + } + } +} + +new Inventory().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/examples/Region/Parcels.ts b/examples/Region/Parcels.ts new file mode 100644 index 0000000..de5276c --- /dev/null +++ b/examples/Region/Parcels.ts @@ -0,0 +1,21 @@ +import { ExampleBot } from '../ExampleBot'; + +class Parcels extends ExampleBot +{ + async onConnected() + { + const parcelInMiddle = await this.bot.clientCommands.region.getParcelAt(128, 128); + console.log('Parcel at 128x128 is ' + parcelInMiddle.Name); + + const parcels = await this.bot.clientCommands.region.getParcels(); + console.log('Parcels on region:'); + console.log('========================'); + for (const p of parcels) + { + console.log(p.Name); + } + console.log('========================'); + } +} + +new Parcels().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/examples/Teleports/Teleports.ts b/examples/Teleports/Teleports.ts new file mode 100644 index 0000000..1bd89a4 --- /dev/null +++ b/examples/Teleports/Teleports.ts @@ -0,0 +1,48 @@ +import { ExampleBot } from '../ExampleBot'; +import { LureEvent } from '../../lib/events/LureEvent'; + +class Teleports extends ExampleBot +{ + async onConnected() + { + // "OnLure" event fires when someone tries to teleport us + this.bot.clientEvents.onLure.subscribe(this.onLure.bind(this)); + + // Alternatively we can TP someone else to us + await this.bot.clientCommands.comms.sendTeleport(this.masterAvatar); + } + + async onLure(lureEvent: LureEvent) + { + try + { + const regionInfo = await this.bot.clientCommands.grid.getRegionMapInfo(lureEvent.gridX / 256, lureEvent.gridY / 256); + if (lureEvent.from.toString() === this.masterAvatar) + { + console.log('Accepting teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1) ? '' : 's') + '' + + ' present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage); + try + { + await this.bot.clientCommands.teleport.acceptTeleport(lureEvent); + } + catch (error) + { + console.error('Teleport error:'); + console.error(error); + } + } + else + { + console.log('Ignoring teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1) ? '' : 's') + ' ' + + 'present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage); + } + } + catch (error) + { + console.error('Failed to get region map info:'); + console.error(error); + } + } +} + +new Teleports().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/example/loginParameters.example.json b/examples/loginParameters.example.json similarity index 100% rename from example/loginParameters.example.json rename to examples/loginParameters.example.json diff --git a/lib/classes/commands/CommunicationsCommands.ts b/lib/classes/commands/CommunicationsCommands.ts index a59124d..2969861 100644 --- a/lib/classes/commands/CommunicationsCommands.ts +++ b/lib/classes/commands/CommunicationsCommands.ts @@ -387,15 +387,15 @@ export class CommunicationsCommands extends CommandsBase }); } - startGroupChatSession(sessionID: UUID | string, message: string): Promise + startGroupChatSession(groupID: UUID | string, message: string): Promise { return new Promise((resolve, reject) => { - if (typeof sessionID === 'string') + if (typeof groupID === 'string') { - sessionID = new UUID(sessionID); + groupID = new UUID(groupID); } - if (this.agent.hasChatSession(sessionID)) + if (this.agent.hasChatSession(groupID)) { resolve(); } @@ -410,13 +410,13 @@ export class CommunicationsCommands extends CommandsBase }; im.MessageBlock = { FromGroup: false, - ToAgentID: sessionID, + ToAgentID: groupID, ParentEstateID: 0, RegionID: UUID.zero(), Position: Vector3.getZero(), Offline: 0, Dialog: InstantMessageDialog.SessionGroupStart, - ID: sessionID, + ID: groupID, Timestamp: Math.floor(new Date().getTime() / 1000), FromAgentName: Utils.StringToBuffer(agentName), Message: Utils.StringToBuffer(message), @@ -427,7 +427,7 @@ export class CommunicationsCommands extends CommandsBase }; const waitForJoin = this.currentRegion.clientEvents.onGroupChatSessionJoin.subscribe((event: GroupChatSessionJoinEvent) => { - if (event.sessionID.toString() === sessionID.toString()) + if (event.sessionID.toString() === groupID.toString()) { if (event.success) { diff --git a/tsconfig.json b/tsconfig.json index b5867b2..2f2b3ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "types": ["node"] }, "include": [ - "lib/**/*.ts" + "lib/**/*.ts", + "examples/**/*.ts" ], "exclude": [ "node_modules"