Compare commits
2 Commits
f0a1f53c24
...
623645090d
Author | SHA1 | Date | |
---|---|---|---|
|
623645090d | ||
|
d7ad22b115 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
node_modules
|
node_modules
|
||||||
docs
|
docs
|
||||||
|
storage
|
51
jsdoc.json
51
jsdoc.json
@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"tags": {
|
|
||||||
"allowUnknownTags": true
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"include": ["./src"],
|
|
||||||
"includePattern": ".js$",
|
|
||||||
"excludePattern": "(node_modules/|docs)"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"plugins/markdown"
|
|
||||||
],
|
|
||||||
"opts": {
|
|
||||||
"template": "node_modules/docdash",
|
|
||||||
"encoding": "utf8",
|
|
||||||
"destination": "docs/",
|
|
||||||
"recurse": true,
|
|
||||||
"verbose": true
|
|
||||||
},
|
|
||||||
"markdown": {
|
|
||||||
"parser": "gfm",
|
|
||||||
"hardwrap": true,
|
|
||||||
"idInHeadings": true
|
|
||||||
},
|
|
||||||
"templates": {
|
|
||||||
"cleverLinks": false,
|
|
||||||
"monospaceLinks": false,
|
|
||||||
"default": {
|
|
||||||
"outputSourceFiles": true,
|
|
||||||
"includeDate": false,
|
|
||||||
"useLongnameInNav": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"docdash": {
|
|
||||||
"static": true,
|
|
||||||
"sort": true,
|
|
||||||
"search": true,
|
|
||||||
"collapse": true,
|
|
||||||
"typedefs": true,
|
|
||||||
"removeQuotes": "none",
|
|
||||||
"wrap": true,
|
|
||||||
"menu": {
|
|
||||||
"Git Repository": {
|
|
||||||
"href":"https://git.ssh.surf/mitask/linkup-bot-lib",
|
|
||||||
"target":"_blank",
|
|
||||||
"class":"menu-item",
|
|
||||||
"id":"gitrepo_link"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
20
package.json
20
package.json
@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "linkup-bot-lib",
|
"name": "linkup-bot-lib",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"main": "src/Client.js",
|
"main": "lib/Client.js",
|
||||||
|
"types": "lib/*.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate-docs": "jsdoc --configure jsdoc.json"
|
"prepublishOnly": "typedoc",
|
||||||
|
"build": "typedoc"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [
|
||||||
"author": "",
|
"linkup"
|
||||||
"type": "module",
|
],
|
||||||
|
"author": "LinkUp Team",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -18,7 +21,8 @@
|
|||||||
"serve-drive": "^5.0.8"
|
"serve-drive": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"docdash": "^2.0.2",
|
"@types/b4a": "^1.6.4",
|
||||||
"jsdoc": "^4.0.3"
|
"@types/node": "^20.14.2",
|
||||||
|
"typedoc-plugin-merge-modules": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,46 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Hyperswarm from 'hyperswarm';
|
import Hyperswarm, { PeerDiscovery } from 'hyperswarm';
|
||||||
import EventEmitter from 'node:events';
|
import {TextMessage} from "./message/TextMessage";
|
||||||
import b4a from "b4a";
|
import {FileMessage} from "./message/FileMessage";
|
||||||
import TextMessage from "./message/TextMessage.js";
|
import {AudioMessage} from "./message/AudioMessage";
|
||||||
import FileMessage from "./message/FileMessage.js";
|
import {Message} from "./message/Message";
|
||||||
import AudioMessage from "./message/AudioMessage.js";
|
|
||||||
import Message from "./message/Message.js";
|
|
||||||
import IconMessage from "./message/IconMessage.js";
|
|
||||||
import Corestore from 'corestore';
|
import Corestore from 'corestore';
|
||||||
import Hyperdrive from 'hyperdrive';
|
import Hyperdrive from 'hyperdrive';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
// @ts-ignore
|
||||||
import ServeDrive from 'serve-drive';
|
import ServeDrive from 'serve-drive';
|
||||||
|
import {IconMessage} from "./message/IconMessage";
|
||||||
|
import {TypedEventEmitter} from "./util/TypedEventEmitter";
|
||||||
|
import {LinkUpEvents} from "./LinkUpEvents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the core component of the bot system. It handles connections to the Hyperswarm network, manages message sending and receiving, and emits events for various actions.
|
* This class is the core component of the bot system. It handles connections to the Hyperswarm network, manages message sending and receiving, and emits events for various actions.
|
||||||
* @emits Client#onMessage
|
|
||||||
* @emits Client#onFile
|
|
||||||
* @emits Client#onAudio
|
|
||||||
* @emits Client#onIcon
|
|
||||||
*/
|
*/
|
||||||
class Client extends EventEmitter {
|
export class Client extends TypedEventEmitter<LinkUpEvents> {
|
||||||
|
public botName: string = "";
|
||||||
|
public servePort: number | null = 0;
|
||||||
|
public storagePath: string | undefined;
|
||||||
|
public swarm: Hyperswarm | undefined;
|
||||||
|
public drive: Hyperdrive | undefined;
|
||||||
|
public store: Corestore | undefined;
|
||||||
|
public joinedRooms: Set<string> | undefined;
|
||||||
|
public currentTopic: string | null = null;
|
||||||
|
public botAvatar: string = "";
|
||||||
|
public iconMessage: IconMessage | undefined;
|
||||||
|
public discovery: PeerDiscovery | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} botName The name of the bot.
|
* @param botName The name of the bot.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @constructor
|
* @constructor
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
*/
|
*/
|
||||||
constructor(botName) {
|
constructor(botName: string) {
|
||||||
super();
|
super();
|
||||||
if (!botName) return console.error("Bot Name is not defined!");
|
if (!botName) {
|
||||||
|
console.error("Bot Name is not defined!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.botName = botName;
|
this.botName = botName;
|
||||||
this.swarm = new Hyperswarm();
|
this.swarm = new Hyperswarm();
|
||||||
this.joinedRooms = new Set(); // Track the rooms the bot has joined
|
this.joinedRooms = new Set(); // Track the rooms the bot has joined
|
||||||
@ -75,6 +87,7 @@ class Client extends EventEmitter {
|
|||||||
this.servePort = this.getRandomPort();
|
this.servePort = this.getRandomPort();
|
||||||
const serve = new ServeDrive({
|
const serve = new ServeDrive({
|
||||||
port: this.servePort,
|
port: this.servePort,
|
||||||
|
// @ts-ignore
|
||||||
get: ({ key, filename, version }) => this.drive
|
get: ({ key, filename, version }) => this.drive
|
||||||
});
|
});
|
||||||
await serve.ready();
|
await serve.ready();
|
||||||
@ -87,23 +100,23 @@ class Client extends EventEmitter {
|
|||||||
* @description Returns a random port number.
|
* @description Returns a random port number.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
* @return {Number} Random port number.
|
* @return Random port number.
|
||||||
*/
|
*/
|
||||||
getRandomPort() {
|
getRandomPort(): number {
|
||||||
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
|
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Fetches and sets the bot's avatar from a local file.
|
* @description Fetches and sets the bot's avatar from a local file.
|
||||||
* @param {String} filePath path to the local avatar file.
|
* @param filePath path to the local avatar file.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
*/
|
*/
|
||||||
async fetchAvatar(filePath) {
|
async fetchAvatar(filePath: string) {
|
||||||
try {
|
try {
|
||||||
await this.drive.ready();
|
await this.drive?.ready();
|
||||||
const iconBuffer = fs.readFileSync(filePath);
|
const iconBuffer = fs.readFileSync(filePath);
|
||||||
await this.drive.put(`/icons/${this.botName}.png`, iconBuffer);
|
await this.drive?.put(`/icons/${this.botName}.png`, iconBuffer);
|
||||||
this.botAvatar = `http://localhost:${this.servePort}/icons/${this.botName}.png`;
|
this.botAvatar = `http://localhost:${this.servePort}/icons/${this.botName}.png`;
|
||||||
|
|
||||||
// Cache the icon message
|
// Cache the icon message
|
||||||
@ -119,15 +132,15 @@ class Client extends EventEmitter {
|
|||||||
* @author snxraven
|
* @author snxraven
|
||||||
*/
|
*/
|
||||||
setupSwarm() {
|
setupSwarm() {
|
||||||
this.swarm.on('connection', (peer) => {
|
this.swarm?.on('connection', (peer) => {
|
||||||
// Send the cached icon message to the new peer
|
// Send the cached icon message to the new peer
|
||||||
if (this.iconMessage) {
|
if (this.iconMessage) {
|
||||||
peer.write(this.iconMessage.toJsonString());
|
peer.write(this.iconMessage.toJsonString());
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.on('data', async message => {
|
peer.on('data', async (message: {}) => {
|
||||||
const messageObj = JSON.parse(message.toString());
|
const messageObj = JSON.parse(message.toString());
|
||||||
if (this.joinedRooms.has(messageObj.topic)) { // Process message only if it is from a joined room
|
if (this.joinedRooms?.has(messageObj.topic)) { // Process message only if it is from a joined room
|
||||||
this.currentTopic = messageObj.topic; // Set the current topic from the incoming message
|
this.currentTopic = messageObj.topic; // Set the current topic from the incoming message
|
||||||
|
|
||||||
const msgType = messageObj.type;
|
const msgType = messageObj.type;
|
||||||
@ -137,35 +150,23 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
|
|
||||||
if (msgType === "message")
|
if (msgType === "message")
|
||||||
/**
|
this.emit('onMessage', new TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message));
|
||||||
* Triggered when a new message is received.
|
|
||||||
*
|
|
||||||
* @event Client#onMessage
|
|
||||||
* @property peer - HyperSwarm peer object
|
|
||||||
* @property {TextMessage} textMessage -Class with all of the information about received text message
|
|
||||||
* @example
|
|
||||||
* const bot = new Client("MyBot");
|
|
||||||
* bot.on('onMessage', (peer, message) => {
|
|
||||||
* console.log(`Message from ${message.peerName}: ${message.message}`);
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.emit('onMessage', peer, new TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message));
|
|
||||||
|
|
||||||
if (msgType === "file") {
|
if (msgType === "file") {
|
||||||
const fileBuffer = await this.drive.get(`/files/${messageObj.fileName}`);
|
const fileBuffer = await this.drive?.get(`/files/${messageObj.fileName}`);
|
||||||
/**
|
/**
|
||||||
* Triggered when a file message is received.
|
* Triggered when a file message is received.
|
||||||
*
|
*
|
||||||
* @event Client#onFile
|
* @event Client#onFile
|
||||||
* @property peer - HyperSwarm peer object
|
* @property peer - HyperSwarm peer object
|
||||||
* @property {FileMessage} fileMessage - Class with all of the information about received file
|
* @property {FileMessage} FileMessage - Class with all of the information about received file
|
||||||
* @example
|
* @example
|
||||||
* const bot = new Client("MyBot");
|
* const bot = new Client("MyBot");
|
||||||
* bot.on('onFile', (peer, message) => {
|
* bot.on('onFile', (peer, message) => {
|
||||||
* console.log(`Received file from ${message.peerName}`);
|
* console.log(`Received file from ${message.peerName}`);
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.emit('onFile', peer, new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData));
|
this.emit('onFile', new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msgType === "icon")
|
if (msgType === "icon")
|
||||||
@ -174,62 +175,65 @@ class Client extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @event Client#onIcon
|
* @event Client#onIcon
|
||||||
* @property peer - HyperSwarm peer object
|
* @property peer - HyperSwarm peer object
|
||||||
* @property {IconMessage} iconMessage - Class with all of the information about received peer icon
|
* @property {IconMessage} IconMessage - Class with all of the information about received peer icon
|
||||||
* @example
|
* @example
|
||||||
* const bot = new Client("MyBot");
|
* const bot = new Client("MyBot");
|
||||||
* bot.on('onIcon', (peer, message) => {
|
* bot.on('onIcon', (peer, message) => {
|
||||||
* console.log(`Received new Icon from ${message.peerName}`);
|
* console.log(`Received new Icon from ${message.peerName}`);
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.emit('onIcon', peer, new IconMessage(peerName, peerAvatar, timestamp));
|
this.emit('onIcon', new IconMessage(peerName, peerAvatar, timestamp));
|
||||||
|
|
||||||
if (msgType === "audio") {
|
if (msgType === "audio") {
|
||||||
const audioBuffer = await this.drive.get(`/audio/${messageObj.audioName}`);
|
const audioBuffer = await this.drive?.get(`/audio/${messageObj.audioName}`);
|
||||||
/**
|
/**
|
||||||
* Triggered when an audio message is received.
|
* Triggered when an audio message is received.
|
||||||
*
|
*
|
||||||
* @event Client#onAudio
|
* @event Client#onAudio
|
||||||
* @property peer - HyperSwarm peer object
|
* @property peer - HyperSwarm peer object
|
||||||
* @property {AudioMessage} audioMessage - Class with all of the information about received audio file
|
* @property {AudioMessage} AudioMessage - Class with all of the information about received audio file
|
||||||
* @example
|
* @example
|
||||||
|
* ```js
|
||||||
* const bot = new Client("MyBot");
|
* const bot = new Client("MyBot");
|
||||||
* bot.on('onAudio', (peer, message) => {
|
* bot.on('onAudio', (peer, message) => {
|
||||||
* console.log(`Received audio file from ${message.peerName}`);
|
* console.log(`Received audio file from ${message.peerName}`);
|
||||||
* });
|
* });
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
this.emit('onAudio', peer, new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData));
|
this.emit('onAudio', new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
peer.on('error', e => {
|
peer.on('error', (err: any) => {
|
||||||
this.emit('onError', e);
|
this.emit('onError', err);
|
||||||
console.error(`Connection error: ${e}`);
|
console.error(`Connection error: ${err}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.swarm.on('update', () => {
|
// @ts-ignore
|
||||||
console.log(`Connections count: ${this.swarm.connections.size} / Peers count: ${this.swarm.peers.size}`);
|
this.swarm.on("update", () => {
|
||||||
|
console.log(`Connections count: ${this.swarm?.connections.size} / Peers count: ${this.swarm?.peers.size}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description Joins a specified chat room.
|
* @description Joins a specified chat room.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
* @param {String} chatRoomID Chat room topic string
|
* @param chatRoomID Chat room topic string
|
||||||
*/
|
*/
|
||||||
joinChatRoom(chatRoomID) {
|
joinChatRoom(chatRoomID: string) {
|
||||||
if (!chatRoomID || typeof chatRoomID !== 'string') {
|
if (!chatRoomID) {
|
||||||
console.error("Invalid chat room ID!");
|
console.error("Invalid chat room ID!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.joinedRooms.add(chatRoomID); // Add the room to the list of joined rooms
|
this.joinedRooms?.add(chatRoomID); // Add the room to the list of joined rooms
|
||||||
this.currentTopic = chatRoomID; // Store the current topic
|
this.currentTopic = chatRoomID; // Store the current topic
|
||||||
this.discovery = this.swarm.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true });
|
this.discovery = this.swarm?.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true });
|
||||||
this.discovery.flushed().then(() => {
|
this.discovery?.flushed().then(() => {
|
||||||
console.log(`Bot ${this.botName} joined the chat room.`);
|
console.log(`Bot ${this.botName} joined the chat room.`);
|
||||||
this.emit('onBotJoinRoom');
|
this.emit('onBotJoinRoom', chatRoomID);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,9 +241,9 @@ class Client extends EventEmitter {
|
|||||||
* @description Sends a text message.
|
* @description Sends a text message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @param {String} message Text message to send to the bot's current chat room.
|
* @param message Text message to send to the bot's current chat room.
|
||||||
*/
|
*/
|
||||||
sendTextMessage(message) {
|
sendTextMessage(message: string) {
|
||||||
console.log(`Preparing to send text message: ${message}`);
|
console.log(`Preparing to send text message: ${message}`);
|
||||||
this.sendMessage(TextMessage.new(this, message));
|
this.sendMessage(TextMessage.new(this, message));
|
||||||
}
|
}
|
||||||
@ -248,15 +252,15 @@ class Client extends EventEmitter {
|
|||||||
* @description Sends a file message.
|
* @description Sends a file message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
* @param {String} filePath Path to the file to send.
|
* @param filePath Path to the file to send.
|
||||||
* @param {String} fileType Type of the file to send.
|
* @param fileType Type of the file to send.
|
||||||
*/
|
*/
|
||||||
async sendFileMessage(filePath, fileType) {
|
async sendFileMessage(filePath: string, fileType: string) {
|
||||||
try {
|
try {
|
||||||
await this.drive.ready();
|
await this.drive?.ready();
|
||||||
const fileBuffer = fs.readFileSync(filePath);
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
const fileName = path.basename(filePath);
|
const fileName = path.basename(filePath);
|
||||||
await this.drive.put(`/files/${fileName}`, fileBuffer);
|
await this.drive?.put(`/files/${fileName}`, fileBuffer);
|
||||||
const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`;
|
const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`;
|
||||||
const fileMessage = FileMessage.new(this, fileName, fileUrl, fileType, fileBuffer); // Pass fileBuffer to the new method
|
const fileMessage = FileMessage.new(this, fileName, fileUrl, fileType, fileBuffer); // Pass fileBuffer to the new method
|
||||||
this.sendMessage(fileMessage);
|
this.sendMessage(fileMessage);
|
||||||
@ -269,15 +273,15 @@ class Client extends EventEmitter {
|
|||||||
* @description Sends an audio message.
|
* @description Sends an audio message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author snxraven
|
* @author snxraven
|
||||||
* @param {String} filePath Path to the audio file to send.
|
* @param filePath Path to the audio file to send.
|
||||||
* @param {String} audioType Type of the audio file to send.
|
* @param audioType Type of the audio file to send.
|
||||||
*/
|
*/
|
||||||
async sendAudioMessage(filePath, audioType) {
|
async sendAudioMessage(filePath: string, audioType: string) {
|
||||||
try {
|
try {
|
||||||
await this.drive.ready();
|
await this.drive?.ready();
|
||||||
const audioBuffer = fs.readFileSync(filePath);
|
const audioBuffer = fs.readFileSync(filePath);
|
||||||
const audioName = path.basename(filePath);
|
const audioName = path.basename(filePath);
|
||||||
await this.drive.put(`/audio/${audioName}`, audioBuffer);
|
await this.drive?.put(`/audio/${audioName}`, audioBuffer);
|
||||||
const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`;
|
const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`;
|
||||||
const audioMessage = AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method
|
const audioMessage = AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method
|
||||||
this.sendMessage(audioMessage);
|
this.sendMessage(audioMessage);
|
||||||
@ -291,17 +295,12 @@ class Client extends EventEmitter {
|
|||||||
* @description Sends a generic message.
|
* @description Sends a generic message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @param {Message} message Message class (TextMessage, FileMessage or AudioMessage)
|
* @param message Message class (TextMessage, FileMessage or AudioMessage)
|
||||||
*/
|
*/
|
||||||
sendMessage(message) {
|
sendMessage(message: Message) {
|
||||||
if (!(message instanceof Message)) {
|
|
||||||
console.error(`message does not extend Message class (TextMessage, FileMessage, AudioMessage).`, message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Sending message:", message);
|
console.log("Sending message:", message);
|
||||||
const data = message.toJsonString();
|
const data = message.toJsonString();
|
||||||
const peers = [...this.swarm.connections];
|
const peers = [...this.swarm?.connections];
|
||||||
if (peers.length === 0) {
|
if (peers.length === 0) {
|
||||||
console.warn("No active peer connections found.");
|
console.warn("No active peer connections found.");
|
||||||
return;
|
return;
|
||||||
@ -324,9 +323,7 @@ class Client extends EventEmitter {
|
|||||||
* @author snxraven
|
* @author snxraven
|
||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
await this.swarm.destroy();
|
await this.swarm?.destroy();
|
||||||
console.log(`Bot ${this.botName} disconnected.`);
|
console.log(`Bot ${this.botName} disconnected.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Client;
|
|
24
src/LinkUpEvents.ts
Normal file
24
src/LinkUpEvents.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {TextMessage} from "./message/TextMessage";
|
||||||
|
import {FileMessage} from "./message/FileMessage";
|
||||||
|
import {AudioMessage} from "./message/AudioMessage";
|
||||||
|
import {IconMessage} from "./message/IconMessage";
|
||||||
|
|
||||||
|
export type LinkUpEvents = {
|
||||||
|
/**
|
||||||
|
* Triggered when a new message is received.
|
||||||
|
*
|
||||||
|
* @event Client#onMessage
|
||||||
|
* @property {TextMessage} TextMessage - Class with all of the information about received text message
|
||||||
|
* @example
|
||||||
|
* const bot = new Client("MyBot");
|
||||||
|
* bot.on('onMessage', (message) => {
|
||||||
|
* console.log(`Message from ${message.peerName}: ${message.message}`);
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
'onMessage': [textMessage: TextMessage]
|
||||||
|
'onFile': [fileMessage: FileMessage]
|
||||||
|
'onAudio': [audioMessage: AudioMessage]
|
||||||
|
'onIcon': [iconMessage: IconMessage]
|
||||||
|
'onError': [error: any]
|
||||||
|
'onBotJoinRoom': [currentTopic: string]
|
||||||
|
}
|
@ -1,21 +1,29 @@
|
|||||||
import Message from "./Message.js";
|
/**
|
||||||
import b4a from "b4a";
|
* @module Message
|
||||||
|
*/
|
||||||
|
|
||||||
class AudioMessage extends Message {
|
import {Message} from "./Message";
|
||||||
|
import b4a from "b4a";
|
||||||
|
import {Client} from "../Client";
|
||||||
|
|
||||||
|
export class AudioMessage extends Message {
|
||||||
|
audioUrl: string;
|
||||||
|
audioType: string;
|
||||||
|
audioData: string;
|
||||||
/**
|
/**
|
||||||
* @description Creates a new Audio message.
|
* @description Creates a new Audio message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {String} peerName Peer username
|
* @param peerName Peer username
|
||||||
* @param {String} peerAvatar Peer avatar URL
|
* @param peerAvatar Peer avatar URL
|
||||||
* @param {String} topic Chat room topic string
|
* @param topic Chat room topic string
|
||||||
* @param {Number} timestamp UNIX Timestamp
|
* @param timestamp UNIX Timestamp
|
||||||
* @param {String} audioUrl URL to the audio file
|
* @param audioUrl URL to the audio file
|
||||||
* @param {String} audioType Type of the audio file
|
* @param audioType Type of the audio file
|
||||||
* @param {String} audioData Audio file data in base64 String format
|
* @param audioData Audio file data in base64 String format
|
||||||
*/
|
*/
|
||||||
constructor(peerName, peerAvatar, topic, timestamp, audioUrl, audioType, audioData) {
|
constructor(peerName: string, peerAvatar: string, topic: string | null, timestamp: number, audioUrl: string, audioType: string, audioData: string) {
|
||||||
super("audio", peerName, peerAvatar, topic, timestamp);
|
super("audio", peerName, peerAvatar, topic, timestamp);
|
||||||
this.audioUrl = audioUrl;
|
this.audioUrl = audioUrl;
|
||||||
this.audioType = audioType;
|
this.audioType = audioType;
|
||||||
@ -27,7 +35,7 @@ class AudioMessage extends Message {
|
|||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @returns {String} JSON String with all of the information about the message
|
* @returns {String} JSON String with all of the information about the message
|
||||||
*/
|
*/
|
||||||
toJsonString() {
|
toJsonString(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
...this.toJson(),
|
...this.toJson(),
|
||||||
audioUrl: this.audioUrl,
|
audioUrl: this.audioUrl,
|
||||||
@ -40,16 +48,14 @@ class AudioMessage extends Message {
|
|||||||
* @description Creates a new audio message instance.
|
* @description Creates a new audio message instance.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @param {Client} bot Bot Client class
|
* @param bot Bot Client class
|
||||||
* @param {String} audioUrl URL to the audio file
|
* @param audioUrl URL to the audio file
|
||||||
* @param {String} audioType Type of the audio file
|
* @param audioType Type of the audio file
|
||||||
* @param {Buffer} audioBuffer Audio file data
|
* @param audioBuffer Audio file data
|
||||||
* @returns {AudioMessage} AudioMessage instance.
|
* @returns {AudioMessage} AudioMessage instance.
|
||||||
*/
|
*/
|
||||||
static new(bot, audioUrl, audioType, audioBuffer) {
|
static new(bot: Client, audioUrl: string, audioType: string, audioBuffer: Buffer): AudioMessage {
|
||||||
const audioData = b4a.toString(audioBuffer, 'base64'); // Convert audio buffer to base64
|
const audioData = b4a.toString(audioBuffer, 'base64'); // Convert audio buffer to base64
|
||||||
return new AudioMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), audioUrl, audioType, audioData);
|
return new AudioMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), audioUrl, audioType, audioData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AudioMessage;
|
|
@ -1,22 +1,31 @@
|
|||||||
import Message from "./Message.js";
|
/**
|
||||||
import b4a from "b4a";
|
* @module Message
|
||||||
|
*/
|
||||||
|
|
||||||
class FileMessage extends Message {
|
import {Message} from "./Message";
|
||||||
|
import b4a from "b4a";
|
||||||
|
import {Client} from "../Client";
|
||||||
|
|
||||||
|
export class FileMessage extends Message {
|
||||||
|
fileName: string;
|
||||||
|
fileUrl: string;
|
||||||
|
fileType: string;
|
||||||
|
fileData: string;
|
||||||
/**
|
/**
|
||||||
* @description Creates a new file message.
|
* @description Creates a new file message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {String} peerName Peer username
|
* @param peerName Peer username
|
||||||
* @param {String} peerAvatar Peer avatar URL
|
* @param peerAvatar Peer avatar URL
|
||||||
* @param {String} topic Chat room topic string
|
* @param topic Chat room topic string
|
||||||
* @param {Number} timestamp UNIX Timestamp
|
* @param timestamp UNIX Timestamp
|
||||||
* @param {String} fileName File name
|
* @param fileName File name
|
||||||
* @param {String} fileUrl URL to the file
|
* @param fileUrl URL to the file
|
||||||
* @param {String} fileType Type of the file
|
* @param fileType Type of the file
|
||||||
* @param {String} fileData File data in base64 String format
|
* @param fileData File data in base64 String format
|
||||||
*/
|
*/
|
||||||
constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType, fileData) {
|
constructor(peerName: string, peerAvatar: string, topic: string | null, timestamp: number, fileName: string, fileUrl: string, fileType: string, fileData: string) {
|
||||||
super("file", peerName, peerAvatar, topic, timestamp);
|
super("file", peerName, peerAvatar, topic, timestamp);
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
this.fileUrl = fileUrl;
|
this.fileUrl = fileUrl;
|
||||||
@ -29,7 +38,7 @@ class FileMessage extends Message {
|
|||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @returns {String} JSON String with all of the information about the message
|
* @returns {String} JSON String with all of the information about the message
|
||||||
*/
|
*/
|
||||||
toJsonString() {
|
toJsonString(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
...this.toJson(),
|
...this.toJson(),
|
||||||
fileName: this.fileName,
|
fileName: this.fileName,
|
||||||
@ -43,17 +52,15 @@ class FileMessage extends Message {
|
|||||||
* @description Creates a new file message instance.
|
* @description Creates a new file message instance.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @param {Client} bot Bot Client class
|
* @param bot Bot Client class
|
||||||
* @param {String} fileName File name
|
* @param fileName File name
|
||||||
* @param {String} fileUrl URL to the file
|
* @param fileUrl URL to the file
|
||||||
* @param {String} fileType Type of the file
|
* @param fileType Type of the file
|
||||||
* @param {Buffer} fileBuffer File data
|
* @param fileBuffer File data
|
||||||
* @returns {FileMessage} FileMessage instance.
|
* @returns {FileMessage} FileMessage instance.
|
||||||
*/
|
*/
|
||||||
static new(bot, fileName, fileUrl, fileType, fileBuffer) {
|
static new(bot: Client, fileName: string, fileUrl: string, fileType: string, fileBuffer: Buffer): FileMessage {
|
||||||
const fileData = b4a.toString(fileBuffer, 'base64'); // Convert file buffer to base64
|
const fileData = b4a.toString(fileBuffer, 'base64'); // Convert file buffer to base64
|
||||||
return new FileMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), fileName, fileUrl, fileType, fileData);
|
return new FileMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), fileName, fileUrl, fileType, fileData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileMessage;
|
|
@ -1,31 +0,0 @@
|
|||||||
import Message from "./Message.js";
|
|
||||||
import b4a from "b4a";
|
|
||||||
|
|
||||||
class IconMessage extends Message {
|
|
||||||
/**
|
|
||||||
* @description Creates a new icon message.
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
* @constructor
|
|
||||||
* @param {String} peerName Peer username
|
|
||||||
* @param {String} peerAvatar Peer avatar URL
|
|
||||||
* @param {Number} timestamp UNIX Timestamp
|
|
||||||
*/
|
|
||||||
constructor(peerName, peerAvatar, timestamp) {
|
|
||||||
super("icon", peerName, peerAvatar, null, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Creates a new icon message instance.
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
* @param {Client} bot Bot Client class
|
|
||||||
* @param {String} avatarBuffer Bot Avatar buffer
|
|
||||||
* @returns {IconMessage} IconMessage instance
|
|
||||||
*/
|
|
||||||
static new(bot, avatarBuffer) {
|
|
||||||
return new IconMessage(bot.botName, b4a.toString(avatarBuffer, 'base64'), Date.now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IconMessage;
|
|
34
src/message/IconMessage.ts
Normal file
34
src/message/IconMessage.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* @module Message
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Message} from "./Message";
|
||||||
|
import b4a from "b4a";
|
||||||
|
import {Client} from "../Client";
|
||||||
|
|
||||||
|
export class IconMessage extends Message {
|
||||||
|
/**
|
||||||
|
* @description Creates a new icon message.
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @constructor
|
||||||
|
* @param peerName Peer username
|
||||||
|
* @param peerAvatar Peer avatar URL
|
||||||
|
* @param timestamp UNIX Timestamp
|
||||||
|
*/
|
||||||
|
constructor(peerName: string, peerAvatar: string, timestamp: number) {
|
||||||
|
super("icon", peerName, peerAvatar, null, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Creates a new icon message instance.
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @param bot Bot Client class
|
||||||
|
* @param avatarBuffer Bot Avatar buffer
|
||||||
|
* @returns IconMessage instance
|
||||||
|
*/
|
||||||
|
static new(bot: Client, avatarBuffer: Buffer): IconMessage {
|
||||||
|
return new IconMessage(bot.botName, b4a.toString(avatarBuffer, 'base64'), Date.now());
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* @description Base class for all messages
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
*/
|
|
||||||
class Message {
|
|
||||||
/**
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
* @constructor
|
|
||||||
* @param {String} messageType Type of the message (text, file, audio, icon)
|
|
||||||
* @param {String} peerName Peer username
|
|
||||||
* @param {String} peerAvatar Peer avatar URL
|
|
||||||
* @param {String} topic Chat room topic string
|
|
||||||
* @param {Number} timestamp UNIX Timestamp
|
|
||||||
*/
|
|
||||||
constructor(messageType, peerName, peerAvatar, topic, timestamp) {
|
|
||||||
this.type = messageType;
|
|
||||||
this.peerName = peerName;
|
|
||||||
this.peerAvatar = peerAvatar;
|
|
||||||
this.topic = topic;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
* @returns {{name: String, topic: String, avatar: String, type: String, timestamp: Number}} JSON Object with all of the information about the message
|
|
||||||
*/
|
|
||||||
toJson() {
|
|
||||||
return {
|
|
||||||
type: this.type,
|
|
||||||
name: this.peerName,
|
|
||||||
avatar: this.peerAvatar,
|
|
||||||
topic: this.topic,
|
|
||||||
timestamp: this.timestamp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 1.0
|
|
||||||
* @author MiTask
|
|
||||||
* @returns {String} JSON String with all of the information about the message
|
|
||||||
*/
|
|
||||||
toJsonString() {
|
|
||||||
return JSON.stringify({
|
|
||||||
...this.toJson()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Message;
|
|
57
src/message/Message.ts
Normal file
57
src/message/Message.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* @description Base class for all messages
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @module Message
|
||||||
|
*/
|
||||||
|
export class Message {
|
||||||
|
type: string;
|
||||||
|
peerName: string;
|
||||||
|
peerAvatar: string;
|
||||||
|
topic: string | null;
|
||||||
|
timestamp: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @constructor
|
||||||
|
* @param messageType Type of the message (text, file, audio, icon)
|
||||||
|
* @param peerName Peer username
|
||||||
|
* @param peerAvatar Peer avatar URL
|
||||||
|
* @param topic Chat room topic string
|
||||||
|
* @param timestamp UNIX Timestamp
|
||||||
|
*/
|
||||||
|
constructor(messageType: string, peerName: string, peerAvatar: string, topic: string | null, timestamp: number) {
|
||||||
|
this.type = messageType;
|
||||||
|
this.peerName = peerName;
|
||||||
|
this.peerAvatar = peerAvatar;
|
||||||
|
this.topic = topic;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @returns JSON Object with all of the information about the message
|
||||||
|
*/
|
||||||
|
protected toJson(): {name: String, topic: String, avatar: String, type: String, timestamp: Number} {
|
||||||
|
return {
|
||||||
|
type: this.type,
|
||||||
|
name: this.peerName,
|
||||||
|
avatar: this.peerAvatar,
|
||||||
|
topic: this.topic ? this.topic : "",
|
||||||
|
timestamp: this.timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0
|
||||||
|
* @author MiTask
|
||||||
|
* @returns {string} JSON String with all of the information about the message
|
||||||
|
*/
|
||||||
|
toJsonString(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
...this.toJson()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,24 @@
|
|||||||
import Message from "./Message.js";
|
/**
|
||||||
|
* @module Message
|
||||||
|
*/
|
||||||
|
|
||||||
class TextMessage extends Message {
|
import {Message} from "./Message";
|
||||||
|
import {Client} from "../Client";
|
||||||
|
|
||||||
|
export class TextMessage extends Message {
|
||||||
|
message: string;
|
||||||
/**
|
/**
|
||||||
* @description Creates a new text message.
|
* @description Creates a new text message.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {String} peerName Peer username
|
* @param peerName Peer username
|
||||||
* @param {String} peerAvatar Peer avatar URL
|
* @param peerAvatar Peer avatar URL
|
||||||
* @param {String} topic Chat room topic string
|
* @param topic Chat room topic string
|
||||||
* @param {Number} timestamp UNIX Timestamp
|
* @param timestamp UNIX Timestamp
|
||||||
* @param {String} message Text of the message
|
* @param message Text of the message
|
||||||
*/
|
*/
|
||||||
constructor(peerName, peerAvatar, topic, timestamp, message) {
|
constructor(peerName: string, peerAvatar: string, topic: string | null, timestamp: number, message: string) {
|
||||||
super("message", peerName, peerAvatar, topic, timestamp);
|
super("message", peerName, peerAvatar, topic, timestamp);
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
@ -22,7 +28,7 @@ class TextMessage extends Message {
|
|||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @returns {String} JSON String with all of the information about the message
|
* @returns {String} JSON String with all of the information about the message
|
||||||
*/
|
*/
|
||||||
toJsonString() {
|
toJsonString(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
...this.toJson(),
|
...this.toJson(),
|
||||||
message: this.message,
|
message: this.message,
|
||||||
@ -33,13 +39,11 @@ class TextMessage extends Message {
|
|||||||
* @description Creates a new text message instance.
|
* @description Creates a new text message instance.
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author MiTask
|
* @author MiTask
|
||||||
* @param {Client} bot Bot Client class
|
* @param bot Bot Client class
|
||||||
* @param {String} message Text of the message
|
* @param message Text of the message
|
||||||
* @returns {TextMessage} TextMessage instance
|
* @returns {TextMessage} TextMessage instance
|
||||||
*/
|
*/
|
||||||
static new(bot, message) {
|
static new(bot: Client, message: string): TextMessage {
|
||||||
return new TextMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), message);
|
return new TextMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TextMessage;
|
|
39
src/util/TypedEventEmitter.ts
Normal file
39
src/util/TypedEventEmitter.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {EventEmitter} from "stream";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used for TypeSafe events.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class TypedEventEmitter<TEvents extends Record<string, any>> {
|
||||||
|
private emitter = new EventEmitter()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
emit<TEventName extends keyof TEvents & string>(
|
||||||
|
eventName: TEventName,
|
||||||
|
...eventArg: TEvents[TEventName]
|
||||||
|
) {
|
||||||
|
this.emitter.emit(eventName, ...(eventArg as []))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
on<TEventName extends keyof TEvents & string>(
|
||||||
|
eventName: TEventName,
|
||||||
|
handler: (...eventArg: TEvents[TEventName]) => void
|
||||||
|
) {
|
||||||
|
this.emitter.on(eventName, handler as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
off<TEventName extends keyof TEvents & string>(
|
||||||
|
eventName: TEventName,
|
||||||
|
handler: (...eventArg: TEvents[TEventName]) => void
|
||||||
|
) {
|
||||||
|
this.emitter.off(eventName, handler as any)
|
||||||
|
}
|
||||||
|
}
|
19
typedoc.json
Normal file
19
typedoc.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"out": "./docs",
|
||||||
|
"includes": "./src",
|
||||||
|
"entryPoints": ["./src/**/*"],
|
||||||
|
"exclude": ["./src/util/*"],
|
||||||
|
"entryPointStrategy": "expand",
|
||||||
|
"cleanOutputDir": true,
|
||||||
|
"jsDocCompatibility": true,
|
||||||
|
"tsconfig": "tsconfig.json",
|
||||||
|
"githubPages": false,
|
||||||
|
"plugin": ["typedoc-plugin-merge-modules"],
|
||||||
|
"excludeReferences": true,
|
||||||
|
"mergeModulesRenameDefaults": true,
|
||||||
|
"mergeModulesMergeMode": "module-category",
|
||||||
|
"emit": "both",
|
||||||
|
"disableSources": true,
|
||||||
|
"excludeExternals": true,
|
||||||
|
"excludeInternal": true
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user