Refactor examples into a better form factor
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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}));
|
||||
18
examples/Camera/Camera.ts
Normal file
18
examples/Camera/Camera.ts
Normal file
@@ -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) });
|
||||
171
examples/ExampleBot.ts
Normal file
171
examples/ExampleBot.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
58
examples/Friends/Friends.ts
Normal file
58
examples/Friends/Friends.ts
Normal file
@@ -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) });
|
||||
96
examples/Groups/Group.ts
Normal file
96
examples/Groups/Group.ts
Normal file
@@ -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) });
|
||||
80
examples/Groups/GroupChat.ts
Normal file
80
examples/Groups/GroupChat.ts
Normal file
@@ -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) });
|
||||
35
examples/InstantMessages/InstantMessages.ts
Normal file
35
examples/InstantMessages/InstantMessages.ts
Normal file
@@ -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) });
|
||||
134
examples/Inventory/Inventory.ts
Normal file
134
examples/Inventory/Inventory.ts
Normal file
@@ -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) });
|
||||
21
examples/Region/Parcels.ts
Normal file
21
examples/Region/Parcels.ts
Normal file
@@ -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) });
|
||||
48
examples/Teleports/Teleports.ts
Normal file
48
examples/Teleports/Teleports.ts
Normal file
@@ -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) });
|
||||
@@ -387,15 +387,15 @@ export class CommunicationsCommands extends CommandsBase
|
||||
});
|
||||
}
|
||||
|
||||
startGroupChatSession(sessionID: UUID | string, message: string): Promise<void>
|
||||
startGroupChatSession(groupID: UUID | string, message: string): Promise<void>
|
||||
{
|
||||
return new Promise<void>((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)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts"
|
||||
"lib/**/*.ts",
|
||||
"examples/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
Reference in New Issue
Block a user