I'm trying out some dynamic options for storing environment variables throughout a distributed app. I'm struggling with combining a simple Socket.io with chat page with connection based on a URL from a config.json file. I thought it would be clever on page load to fetch the json file and feed the URL to the socket object. However, I keep running into a chicken and egg issue with the async processing. With what I have right now it starts the connection with the startSocket() function but I would like to have the socket variable available for other functions such as the event listener for the message form. Is there a better way to handle the Socket object get me out of this mess? Obviously new to web dev.
const messageContainer = document.getElementById('message-container')
const messageForm = document.getElementById('send-container')
const messageInput = document.getElementById('message-input')
const name = document.getElementById('chatUserField').getAttribute('data-chatUsername');
fetch('./assets/config.json')
.then(results => results.json())
.then(data => {
startSocket(JSON.stringify(data.SocketIO_URL).replace(/['"]+/g, ''))
})
.catch(err=>console.log(err))
function startSocket(url) {
const socket = io(url)
appendMessage('you joined')
socket.emit('new-user', name)
socket.on('chat-message', data => {
appendMessage(`${data.name}: ${data.message}`)
})
socket.on('user-connected', name => {
appendMessage(`${name} connected`)
})
socket.on('user-disconnected', name => {
appendMessage(`${name} disconnected`)
})
}
messageForm.addEventListener('submit', e => {
e.preventDefault()
const message = messageInput.value
appendMessage(`You: ${message}`)
socket.emit('send-chat-message', message)
messageInput.value = ''
})
function appendMessage(message) {
const messageElement = document.createElement('div')
messageElement.innerText = message
messageContainer.append(messageElement)
}
Error:
ReferenceError: socket is not defined
There's a few things going on here. The reason why you're getting the error, I presume, is not because of the async question you describe—rather it's because in the event listener, you have the following line:
socket.emit('send-chat-message', message)
However, socket isn't defined here within that function. You defined it in the startSocket function but it's locally scoped, meaning that outside of the code in startSocket, you can't access it.
Generally you want to add event listeners and all the stuff that depends on the loaded config in the promise .then handler, since JavaScript will then only run that when your config file is loaded (which is what you want!).
So I would return the socket object from startSocket:
function startSocket(url) {
const socket = io(url)
appendMessage('you joined')
socket.emit('new-user', name)
socket.on('chat-message', data => {
appendMessage(`${data.name}: ${data.message}`)
})
socket.on('user-connected', name => {
appendMessage(`${name} connected`)
})
socket.on('user-disconnected', name => {
appendMessage(`${name} disconnected`)
})
return socket;
}
Then I would assign it to a variable in the .then handler.
fetch('./assets/config.json')
.then(results => results.json())
.then(data => {
// do all processing related to setting stuff up now here
const mySocket = startSocket(JSON.stringify(data.SocketIO_URL).replace(/['"]+/g, ''));
// now that we have the socket object, let's set up the event listeners
messageForm.addEventListener('submit', e => {
e.preventDefault()
const message = messageInput.value
appendMessage(`You: ${message}`)
mySocket.emit('send-chat-message', message)
messageInput.value = ''
})
})
Related
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.
Im trying to make a discord bot that shows my minecraft server stats and stuff. It is almost done but when i do the command it comes up with this in the terminal: TypeError: ping is not a function. Here is my code:
const {Client, RichEmbed } = require('discord.js')
const bot = new Client()
const ping = require('minecraft-server-util')
const token = 'not gunna tell u my token'
const ip = 'or ip'
const PREFIX = '!'
bot.on('ready', () =>{
console.log('Bot has come online.')
})
bot.on('message', message =>{
let args = message.content.substring(PREFIX.length).split(' ')
switch(args[0]){
case 'mc':
ping(ip, parseInt(25565), (error, reponse) =>{
if(error) throw error
const Embed = new RichEmbed()
.setTitle('Server Status')
.addField('Server IP', reponse.host)
.addField('Server Version', reponse.version)
.addField('Online Players', reponse.onlinePlayers)
.addField('Max Players', reponse.maxPlayers)
message.channel.send(Embed)
})
break
}
})
bot.login(token)
As the docs say, ping is a property of that module's exports: it's not the whole export:
const util = require('minecraft-server-util');
util.ping('play.hypixel.net', { port: 25565 })
For your code, either destructure out the ping property on import:
const { ping } = require('minecraft-server-util')
Or give it some other name, then call .ping on the imported object:
const util = require('minecraft-server-util');
// ...
util.ping(ip, /* etc */
Also, if you want the port to be the default 25565, there's no need to pass it at all. The module also returns a Promise, which you should use instead of the callback form:
ping(ip)
.then((response) => {
// handle response
})
.catch((error) => {
// handle errors
});
I want to isolate some login inside of a child process. The idea is pretty simple:
I wait for some event inside of master process
send a message to child process
if child process are able to handle it, then receive a result
if child process fails then, log erros and fork a new process
The problem here: messaging. So, I wrote a prototype solution bellow:
const minion = fork('./minion')
const setupSend = (emmiter) => {
const pool = {}
emmiter.on('message', ({id, msg}) => {
pool[id](msg)
delete pool[id]
})
const send = (msg) => {
const id = getId()
const refObj = {}
const p = new Promise((resolve) => {
refObj.resolve = resolve
})
pool[id] = refObj.resolve
emmiter.send({id , msg})
return p
}
return send
}
const send = setupSend(minion)
send('message to reverse').then((result) => {
console.log(result)
})
and sample minion code:
process.on('message', ({id, msg}) => {
process.send({id, msg: msg.split("").reverse().join("")})
});
It works but it doesn't handle the errors and exit cases. probably I will manage to write all the required logic, but it feels like I am inventing a wheel.
So, is there an easier way to achieve this functionality?
I'm replicating this Google authored tutorial and I have run into a problem and error that I can't figure out how to resolve.
On the Google Cloud Function import json to bigquery, I get an error " TypeError: job.promise is not a function "
Which is located towards the bottom of the function, the code in question is:
.then(([job]) => job.promise())
The error led me to this discussion about the API used, but I don't understand how to resolve the error.
I tried .then(([ job ]) => waitJobFinish(job)) and removing the line resolves the error but doesn't insert anything.
Tertiary question: I also can't find documentation on how to trigger a test of the function so that I can read my console.logs in the google cloud function console, which would help to figure this out . I can test the json POST part of this function, but I can't find what json to trigger a test of a new file write to cloud storage - the test says must include a bucket but I don't know what json to format (the json I use to test the post -> store to cloud storage doesn't work)
Here is the full function which I've pulled into it's own function:
(function () {
'use strict';
// Get a reference to the Cloud Storage component
const storage = require('#google-cloud/storage')();
// Get a reference to the BigQuery component
const bigquery = require('#google-cloud/bigquery')();
function getTable () {
const dataset = bigquery.dataset("iterableToBigquery");
return dataset.get({ autoCreate: true })
.then(([dataset]) => dataset.table("iterableToBigquery").get({ autoCreate: true }));
}
//set trigger for new files to google storage bucket
exports.iterableToBigquery = (event) => {
const file = event.data;
if (file.resourceState === 'not_exists') {
// This was a deletion event, we don't want to process this
return;
}
return Promise.resolve()
.then(() => {
if (!file.bucket) {
throw new Error('Bucket not provided. Make sure you have a "bucket" property in your request');
} else if (!file.name) {
throw new Error('Filename not provided. Make sure you have a "name" property in your request');
}
return getTable();
})
.then(([table]) => {
const fileObj = storage.bucket(file.bucket).file(file.name);
console.log(`Starting job for ${file.name}`);
const metadata = {
autodetect: true,
sourceFormat: 'NEWLINE_DELIMITED_JSON'
};
return table.import(fileObj, metadata);
})
.then(([job]) => job.promise())
//.then(([ job ]) => waitJobFinish(job))
.then(() => console.log(`Job complete for ${file.name}`))
.catch((err) => {
console.log(`Job failed for ${file.name}`);
return Promise.reject(err);
});
};
}());
So I couldn't figure out how to fix google's example, but I was able to get this load from js to work with the following code in google cloud function:
'use strict';
/*jshint esversion: 6 */
// Get a reference to the Cloud Storage component
const storage = require('#google-cloud/storage')();
// Get a reference to the BigQuery component
const bigquery = require('#google-cloud/bigquery')();
exports.iterableToBigquery = (event) => {
const file = event.data;
if (file.resourceState === 'not_exists') {
// This was a deletion event, we don't want to process this
return;
}
const importmetadata = {
autodetect: false,
sourceFormat: 'NEWLINE_DELIMITED_JSON'
};
let job;
// Loads data from a Google Cloud Storage file into the table
bigquery
.dataset("analytics")
.table("iterable")
.import(storage.bucket(file.bucket).file(file.name),importmetadata)
.then(results => {
job = results[0];
console.log(`Job ${job.id} started.`);
// Wait for the job to finish
return job;
})
.then(metadata => {
// Check the job's status for errors
const errors = metadata.status.errors;
if (errors && errors.length > 0) {
throw errors;
}
})
.then(() => {
console.log(`Job ${job.id} completed.`);
})
.catch(err => {
console.error('ERROR:', err);
});
};
I am following the answer of #dstoiko from here
I am calling the API in ADD_MOVIE block and want to pass some value to my postback with payload ADD_TO_FIREBASE
here is my blocks
'use strict';
const Script = require('smooch-bot').Script;
var YtsHelper = require('./libs/YtsHelper.js');
const FirebaseHelper = require('./libs/FirebaseHelper.js');
var firebaseHelperObj = new FirebaseHelper();
module.exports = new Script({
processing: {
prompt: (bot) => bot.say('Beep boop...'),
receive: () => 'processing'
},
start: {
receive: (bot) => {
return bot.say('Hi! I\'m Smooch Bot!')
.then(() => 'showUserMenu');
}
},
showUserMenu: {
prompt: (bot) => bot.say("Here are the areas I can help you out. %[Add Movie](postback:ADD_MOVIE) %[Serve Food](postback:SERVE_FOOD)"),
receive: () => 'finish'
},
ADD_MOVIE : {
prompt: (bot) => bot.say('Enter movie name or keywords you want to search please.'),
receive: (bot, message) => {
const movie_name_searched = message.text;
return bot.setProp('movie_name_searched', movie_name_searched)
.then(() => bot.say('Search in progress...'))
.then(() => {
YtsHelper.getMoviesList(movie_name_searched,function(movies_array){
var movies_postbacks = "";
console.log("Movies SIZE " + movies_array.length);
for (var i = 0; i < movies_array.length ; i++){
movies_postbacks = movies_postbacks + " %["+movies_array[i]+"](postback:ADD_TO_FIREBASE)";
}
bot.say(movies_postbacks)
.then(() => bot.say("Click any movie to add into firebase."));
});
});
}
},
ADD_TO_FIREBASE: {
prompt: (bot) => bot.say("confirm, y/n"),
receive: () => 'showUserMenu'
},
finish: {
receive: (bot, message) => {
return bot.getProp('name')
.then((name) => bot.say(`Sorry ${name}, my creator didn't ` +
'teach me how to do anything else!'))
.then(() => 'showUserMenu');
}
}
});
Questions
Q0. I am new to nodeJS also, What should I call ADD_MOVIE, start, showUserMenu (in my code) blocks? function, method, code, module etc.
Q1. I Have called an yts api in my ADD_MOVIE block. Is this fine to call API in script.js file?
Q2. Important!: How can I pass the param to my postback with payload ADD_MOVIE so that I can perform some conditional code in ADD_TO_FIREBASE block
Q0: is a question of style, there's no definitive answer to give here. In other words, this is the wrong forum for this kind of discussion :) https://stackoverflow.com/help/how-to-ask
Q1: Yes making a DB query in receive is fine, however your receive function isn't waiting for the query to finish before it resolves your bot state. If for example you don't want your bot to accept user input until after the movie list is returned, you could do this:
receive: (bot, message) => {
const movie_name_searched = message.text;
return bot.setProp('movie_name_searched', movie_name_searched)
.then(() => bot.say('Search in progress...'))
.then(() => {
return new Promise((res) => YtsHelper.getMoviesList(movie_name_searched, (movies_array) => res(movies_array)));
})
.then((movies_array) => {
var movies_postbacks = "";
for (var i = 0; i < movies_array.length ; i++){
movies_postbacks = movies_postbacks + " %["+movies_array[i]+"](postback:ADD_TO_FIREBASE)";
}
return bot.say(movies_postbacks);
})
.then(() => bot.say("Click any movie to add into firebase."))
.then(() => 'ADD_MOVIE');
}
Note that I'm resolving the very end of the promise chain with 'ADD_MOVIE', which tells your bot to remain in the same state as it was before.
Q2: I see two options.
Option 1: Append the movie ID to the postback payload, eg ADD_TO_FIREBASE.movieid1, ADD_TO_FIREBASE.movieid2 and so on..
If you did this, you would have to define your own behavior inside handlePostback that parses out the movie ID from your postback payload.
You would also have to transition your state amchine into the desired ADD_TO_FIREBASE state yourself. Eg, from your custom handlePostback methdod you would do something like this:
const stateMachine = new StateMachine({
script,
bot: createBot(req.body.appUser)
});
stateMachine.setState('ADD_TO_FIREBASE');
Option 2: The %[foo](postback:bar) message you're using is actually a shorthand syntax. The real inner workings of postback messages are action buttons which you can send to the Smooch API directly. Action buttons also allow you to specify a metadata object. If instead of using the built-in bot.say, you could post messages ot the API directly, and you could store your movie IDs inside the action metadata. You would again have to retrieve the selected movieId from this metadata via your custom handlePostback as you did in option 1.