Stable joining with dummy commands sent on timeouts
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -292,3 +292,4 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
/.vscode
|
||||||
@@ -13,8 +13,9 @@ async function boot() {
|
|||||||
// Run the first time boot sequence
|
// Run the first time boot sequence
|
||||||
await firstTimeBoot();
|
await firstTimeBoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the socket connection with the server
|
// Initialize the socket connection with the server
|
||||||
return initSocketConnection();
|
return initSocketConnection(localNodeConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,7 @@ async function firstTimeBoot() {
|
|||||||
updateId(localNodeConfig.node.id);
|
updateId(localNodeConfig.node.id);
|
||||||
console.log("Updated the config with the new node ID");
|
console.log("Updated the config with the new node ID");
|
||||||
// TODO - Create the config file with the ID given and replace the update above
|
// TODO - Create the config file with the ID given and replace the update above
|
||||||
|
// TODO - Check if the system is linux or windows and set the 'type' param in DAB
|
||||||
|
|
||||||
// TODO - Implement web server so users can update radio systems easily
|
// TODO - Implement web server so users can update radio systems easily
|
||||||
// TODO - Implement logic to check if the presets are set
|
// TODO - Implement logic to check if the presets are set
|
||||||
@@ -39,7 +41,7 @@ async function firstTimeBoot() {
|
|||||||
|
|
||||||
// Boot the client application
|
// Boot the client application
|
||||||
boot().then((openSocket) => {
|
boot().then((openSocket) => {
|
||||||
initSocketListeners(openSocket);
|
initSocketListeners(openSocket, localNodeConfig);
|
||||||
//console.log(openSocket, "Open socket");
|
//console.log(openSocket, "Open socket");
|
||||||
setTimeout(() => {sendNodeUpdate(openSocket);}, 2500);
|
setTimeout(() => {sendNodeUpdate(openSocket);}, 2500);
|
||||||
})
|
})
|
||||||
123
client/discordAudioBot/dab.mjs
Normal file
123
client/discordAudioBot/dab.mjs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
NoSubscriberBehavior,
|
||||||
|
StreamType,
|
||||||
|
createAudioPlayer,
|
||||||
|
createAudioResource,
|
||||||
|
entersState,
|
||||||
|
AudioPlayerStatus,
|
||||||
|
VoiceConnectionStatus,
|
||||||
|
joinVoiceChannel,
|
||||||
|
} from '@discordjs/voice';
|
||||||
|
|
||||||
|
import { GatewayIntentBits } from 'discord-api-types/v10';
|
||||||
|
|
||||||
|
import { Client, Events } from 'discord.js';
|
||||||
|
|
||||||
|
import prism_media from 'prism-media';
|
||||||
|
const { FFmpeg } = prism_media;
|
||||||
|
|
||||||
|
const device = "VoiceMeeter VAIO3 Output (VB-Audio VoiceMeeter VAIO3)", maxTransmissionGap = 500, type = "dshow";
|
||||||
|
|
||||||
|
const player = createAudioPlayer({
|
||||||
|
behaviors: {
|
||||||
|
noSubscriber: NoSubscriberBehavior.Play,
|
||||||
|
maxMissedFrames: Math.round(maxTransmissionGap / 20),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function attachRecorder() {
|
||||||
|
player.play(
|
||||||
|
createAudioResource(
|
||||||
|
new FFmpeg({
|
||||||
|
args: [
|
||||||
|
'-analyzeduration',
|
||||||
|
'0',
|
||||||
|
'-loglevel',
|
||||||
|
'0',
|
||||||
|
'-f',
|
||||||
|
type,
|
||||||
|
'-i',
|
||||||
|
type === 'dshow' ? `audio=${device}` : device,
|
||||||
|
'-acodec',
|
||||||
|
'libopus',
|
||||||
|
'-f',
|
||||||
|
'opus',
|
||||||
|
'-ar',
|
||||||
|
'48000',
|
||||||
|
'-ac',
|
||||||
|
'2',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
inputType: StreamType.OggOpus,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log('Attached recorder - ready to go!');
|
||||||
|
}
|
||||||
|
|
||||||
|
player.on('stateChange', (oldState, newState) => {
|
||||||
|
if (oldState.status === AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing) {
|
||||||
|
console.log('Playing audio output on audio player');
|
||||||
|
} else if (newState.status === AudioPlayerStatus.Idle) {
|
||||||
|
console.log('Playback has stopped. Attempting to restart.');
|
||||||
|
attachRecorder();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {any} channel
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export async function connectToChannel(channel) {
|
||||||
|
const connection = joinVoiceChannel({
|
||||||
|
channelId: channel.id,
|
||||||
|
guildId: channel.guild.id,
|
||||||
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
|
||||||
|
return connection;
|
||||||
|
} catch (error) {
|
||||||
|
connection.destroy();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVoiceChannelFromID(client, channelID) {
|
||||||
|
return client.channels.cache.get(channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initDiscordBotClient(token, readyCallback){
|
||||||
|
const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent],
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on(Events.ClientReady, () => {
|
||||||
|
console.log('discord.js client is ready!');
|
||||||
|
attachRecorder();
|
||||||
|
readyCallback(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on(Events.MessageCreate, async (message) => {
|
||||||
|
if (!message.guild) return;
|
||||||
|
console.log(`New Message:`, message.content);
|
||||||
|
if (message.content === '-join') {
|
||||||
|
const channel = message.member?.voice.channel;
|
||||||
|
if (channel) {
|
||||||
|
try {
|
||||||
|
const connection = await connectToChannel(channel);
|
||||||
|
connection.subscribe(player);
|
||||||
|
await message.reply('Playing now!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await message.reply('Join a voice channel then try again!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(token);
|
||||||
|
}
|
||||||
142
client/modules/radioPresetHandler.js
Normal file
142
client/modules/radioPresetHandler.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// Debug
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
|
const log = new DebugBuilder("client", "updatePresets");
|
||||||
|
// Modules
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require("path");
|
||||||
|
const converter = require("convert-units");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given presets to the JSON file
|
||||||
|
* @param presets The preset or presets to be written
|
||||||
|
* @param {function} callback The function to be called when this wrapper completes
|
||||||
|
*/
|
||||||
|
function writePresets(presets, callback = undefined) {
|
||||||
|
log.DEBUG(`${__dirname}`);
|
||||||
|
fs.writeFile("./config/radioPresets.json", JSON.stringify(presets), (err) => {
|
||||||
|
// Error checking
|
||||||
|
if (err) throw err;
|
||||||
|
log.DEBUG("Write Complete");
|
||||||
|
if (callback) callback(); else return
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to ensure each value in the array is in Hz format
|
||||||
|
* @param frequenciesArray
|
||||||
|
* @returns {*[]}
|
||||||
|
*/
|
||||||
|
function sanitizeFrequencies(frequenciesArray) {
|
||||||
|
let sanitizedFrequencyArray = [];
|
||||||
|
|
||||||
|
for (const freq of frequenciesArray) {
|
||||||
|
sanitizedFrequencyArray.push(convertFrequencyToHertz(freq));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.DEBUG("Sanitized Frequency Array", sanitizedFrequencyArray);
|
||||||
|
return sanitizedFrequencyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to convert a string or a float into the integer type needed to be saved
|
||||||
|
* @param frequency Could be a string, number or float,
|
||||||
|
* @returns {number|number|*} Return the value to be saved in Hz format ("154.875"MHz format = "154875000")
|
||||||
|
*/
|
||||||
|
function convertFrequencyToHertz(frequency){
|
||||||
|
// check if the passed value is a number
|
||||||
|
if(typeof frequency == 'number' && !isNaN(frequency)){
|
||||||
|
if (Number.isInteger(frequency)) {
|
||||||
|
log.DEBUG(`${frequency} is an integer.`);
|
||||||
|
// Check to see if the frequency has the correct length
|
||||||
|
if (frequency >= 1000000) return frequency
|
||||||
|
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
|
||||||
|
log.WARN("Frequency hasn't matched filters: ", frequency);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.DEBUG(`${frequency} is a float value.`);
|
||||||
|
// Convert to a string to remove the decimal in place and then correct the length
|
||||||
|
return parseInt(converter(frequency).from("MHz").to("Hz"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.DEBUG(`${frequency} is not a number`);
|
||||||
|
frequency = convertFrequencyToHertz(parseFloat(frequency));
|
||||||
|
|
||||||
|
return parseInt(frequency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the saved presets and returns a preset object
|
||||||
|
* @returns {any} The object containing the different systems the bot is near
|
||||||
|
*/
|
||||||
|
function getPresets() {
|
||||||
|
const presetDir = path.resolve("./config/radioPresets.json");
|
||||||
|
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
||||||
|
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
|
||||||
|
else return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new preset to the radioPresets JSON file
|
||||||
|
*
|
||||||
|
* @param {string} systemName The name of the system being added
|
||||||
|
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
|
||||||
|
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
|
||||||
|
* @param {function} callback The callback function to call when completed
|
||||||
|
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
|
||||||
|
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
|
||||||
|
*/
|
||||||
|
function addNewPreset(systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) {
|
||||||
|
const presets = this.getPresets();
|
||||||
|
// Create the preset for the new system
|
||||||
|
presets[systemName] = {
|
||||||
|
"frequencies": sanitizeFrequencies(frequencies),
|
||||||
|
"mode": mode,
|
||||||
|
"trunkFile": trunkFile ?? "none",
|
||||||
|
"whitelistFile": whitelistFile ?? "none"
|
||||||
|
}
|
||||||
|
// Write the changes to the preset config file
|
||||||
|
writePresets(presets, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the specified system
|
||||||
|
*
|
||||||
|
* @param {string} systemName The name of the system being modified
|
||||||
|
* @param {function} callback The callback function to be called when the function completes
|
||||||
|
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
|
||||||
|
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
|
||||||
|
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
|
||||||
|
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
|
||||||
|
*/
|
||||||
|
function updatePreset(systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) {
|
||||||
|
const presets = this.getPresets();
|
||||||
|
// Check if a system name was passed
|
||||||
|
if (systemName in presets) {
|
||||||
|
// System name exists, checking to see if the keys are different
|
||||||
|
if(frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies);
|
||||||
|
if(mode && mode !== presets[systemName].mode) presets[systemName].mode = mode;
|
||||||
|
if(trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none";
|
||||||
|
if(whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none";
|
||||||
|
// Write the changes
|
||||||
|
writePresets(presets, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified system
|
||||||
|
*
|
||||||
|
* @param {string} systemName The name of the system being modified
|
||||||
|
* @param {function} callback The callback function to be called when the function completes
|
||||||
|
*/
|
||||||
|
function removePreset(systemName, callback) {
|
||||||
|
const presets = this.getPresets();
|
||||||
|
// Check if a system name was passed
|
||||||
|
if (systemName in presets) {
|
||||||
|
delete presets[systemName];
|
||||||
|
writePresets(presets, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
|
import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID } from '../discordAudioBot/dab.mjs';
|
||||||
|
|
||||||
export function initSocketConnection() {
|
export function initSocketConnection(localNodeConfig) {
|
||||||
const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint
|
const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint
|
||||||
|
|
||||||
const socket = io.connect(serverEndpoint);
|
const socket = io.connect(serverEndpoint);
|
||||||
@@ -8,14 +9,26 @@ export function initSocketConnection() {
|
|||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initSocketListeners(socket){
|
export function initSocketListeners(socket, localNodeConfig) {
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('Connected to the server');
|
console.log('Connected to the server');
|
||||||
logIntoServer(socket);
|
logIntoServer(socket, localNodeConfig.node);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('node-join', (joinData) => {
|
socket.on('node-join', async (joinData) => {
|
||||||
console.log("Join requested: ", joinData)
|
console.log("Join requested: ", joinData)
|
||||||
|
// TODO - Implement logic to control OP25 for the requested channel
|
||||||
|
|
||||||
|
// Join the requested channel with the requested ID
|
||||||
|
initDiscordBotClient(joinData.clientID, client => {
|
||||||
|
console.log("Client:", client);
|
||||||
|
getVoiceChannelFromID(client, joinData.channelID).then(vc => {
|
||||||
|
console.log("Voice Channel:", vc);
|
||||||
|
console.log("Voice Channel Guild:", vc.Guild);
|
||||||
|
const connection = connectToChannel(vc);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log("All done?");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('node-leave', () => {
|
socket.on('node-leave', () => {
|
||||||
|
|||||||
@@ -19,4 +19,7 @@
|
|||||||
"replace-in-file": "^7.1.0",
|
"replace-in-file": "^7.1.0",
|
||||||
"socket.io-client": "^4.7.2"
|
"socket.io-client": "^4.7.2"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { REST, Routes } from 'discord.js';
|
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
//const clientId = process.env.clientId;
|
|
||||||
//const guildId = process.env.guildId;
|
|
||||||
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
var commands = [];
|
|
||||||
// Grab all the command files from the commands directory you created earlier
|
|
||||||
const commandsPath = path.resolve(__dirname, '../commands');
|
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
|
||||||
|
|
||||||
export function deploy (clientId, guildIDs) {
|
|
||||||
console.log("Deploying commands for: ", guildIDs);
|
|
||||||
if (!Array.isArray(guildIDs)) guildIDs = [guildIDs];
|
|
||||||
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
|
|
||||||
for (const file of commandFiles) {
|
|
||||||
const command = require(`${path.resolve(commandsPath, file)}`);
|
|
||||||
console.log('Deploying Command: ', command);
|
|
||||||
commands.push(command.data.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct and prepare an instance of the REST module
|
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
|
||||||
|
|
||||||
// and deploy your commands!
|
|
||||||
for (const guildId of guildIDs) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
console.log(`Started refreshing ${commands.length} application (/) commands for guild ID: ${guildId}.`);
|
|
||||||
// The put method is used to fully refresh all commands in the guild with the current set
|
|
||||||
const data = await rest.put(
|
|
||||||
Routes.applicationGuildCommands(clientId, guildId),
|
|
||||||
{ body: commands },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guildId}.`);
|
|
||||||
} catch (error) {
|
|
||||||
// And of course, make sure you catch and log any errors!
|
|
||||||
console.log("ERROR Deploying commands: ", error, "Body from error: ", commands);
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all commands for a given bot in a given guild
|
|
||||||
*
|
|
||||||
* @param {*} clientId The client ID of the bot to remove commands from
|
|
||||||
* @param {*} guildId The ID of the guild to remove the bot commands from
|
|
||||||
*/
|
|
||||||
export function removeAll (clientId, guildId) {
|
|
||||||
if (!Array.isArray(guildId)) guildIDs = [guildId];
|
|
||||||
console.log("Removing commands for: ", clientId, guildIDs);
|
|
||||||
|
|
||||||
commands = [];
|
|
||||||
|
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
|
||||||
for (const guildId of guildIDs) {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
console.log(`Started refreshing ${commands.length} application (/) commands for guild ID: ${guildId}.`);
|
|
||||||
// The put method is used to fully refresh all commands in the guild with the current set
|
|
||||||
const data = await rest.put(
|
|
||||||
Routes.applicationGuildCommands(clientId, guildId),
|
|
||||||
{ body: commands },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guildId}.`);
|
|
||||||
} catch (error) {
|
|
||||||
// And of course, make sure you catch and log any errors!
|
|
||||||
console.log("ERROR Deploying commands: ", error, "Body from error: ", commands);
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
import { Client, GatewayIntentBits } from 'discord.js';
|
||||||
|
//import { deployActiveCommands } from '../discordBot/modules/deployCommands.mjs'
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,16 @@ nodeIo.on('connection', (socket) => {
|
|||||||
console.log('user disconnected');
|
console.log('user disconnected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Test commands
|
// Test commands
|
||||||
setTimeout(() => { sendNodeCommand(socket, "node-join", { 'some': 'data' }); }, 2500)
|
setTimeout(() => {
|
||||||
setTimeout(() => { sendNodeCommand(socket, "node-leave", {}); }, 3500)
|
const joinData = {
|
||||||
|
'clientID': "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA",
|
||||||
|
'channelID': "367396189529833476",
|
||||||
|
'preset': ""
|
||||||
|
}
|
||||||
|
sendNodeCommand(socket, "node-join", joinData);
|
||||||
|
}, 2500)
|
||||||
|
//setTimeout(() => { sendNodeCommand(socket, "node-leave", {}); }, 3500)
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendNodeCommand(socket, command, data) {
|
function sendNodeCommand(socket, command, data) {
|
||||||
@@ -55,5 +61,4 @@ function updateNodeData(data) {
|
|||||||
|
|
||||||
function nodeLoginWrapper(data) {
|
function nodeLoginWrapper(data) {
|
||||||
console.log(`Login requested from node: ${data.id}`, data);
|
console.log(`Login requested from node: ${data.id}`, data);
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user