Manage a long-running operation node.js - javascript

I am creating a telegram bot, which allows you to get some information about the destiny 2 game world, using the Bungie API. The bot is based on the Bot Framework and uses Telegram as a channel (as a language I am using JavaScript).
now I find myself in the situation where when I send a request to the bot it sends uses series of HTTP calls to the EndPoints of the API to collect information, format it and resubmit it via Adaptive cards, this process however in many cases takes more than 15 seconds showing in chat the message "POST to DestinyVendorBot timed out after 15s" (even if this message is shown the bot works perfectly).
Searching online I noticed that there doesn't seem to be a way to hide this message or increase the time before it shows up. So the only thing left for me to do is to make sure it doesn't show up. To do this I tried to refer to this documentation article. But the code shown is in C #, could someone give me an idea on how to solve this problem of mine or maybe some sample code?
I leave here an example of a call that takes too long and generates the message:
//Mostra l'invetraio dell'armaiolo
if (LuisRecognizer.topIntent(luisResult) === 'GetGunsmith') {
//Take more 15 seconds
const mod = await this.br.getGunsmith(accessdata, process.env.MemberShipType, process.env.Character);
if (mod.error == 0) {
var card = {
}
await step.context.sendActivity({
text: 'Ecco le mod vendute oggi da Banshee-44:',
attachments: [CardFactory.adaptiveCard(card)]
});
} else {
await step.context.sendActivity("Codice di accesso scaduto.");
await this.loginStep(step);
}
}

I have done something similar where you call another function and send the message once the function is complete via proactive message. In my case, I set up the function directly inside the bot instead of as a separate Azure Function. First, you need to save the conversation reference somewhere. I store this in conversation state, and resave it every turn (you could probably do this in onMembersAdded but I chose onMessage when I did it so it resaves the conversation reference every turn). You'll need to import const { TurnContext } = require('botbuilder') for this.
// In your onMessage handler
const conversationData = await this.dialogState.get(context, {});
conversationData.conversationReference = TurnContext.getConversationReference(context.activity);
await this.conversationState.saveChanges(context);
You'll need this for the proactive message. When it's time to send the API, you'll need to send a message (well technically that's optional but recommended), get the conversation data if you haven't gotten it already, and call the API function without awaiting it. If your API is always coming back around 15 seconds, you may just want a standard message (e.g. "One moment while I look that up for you"), but if it's going to be longer I would recommend setting the expectation with the user (e.g. "I will look that up for you. It may take up to a minute to get an answer. In the meantime you can continue to ask me questions."). You should be saving user/conversation state further down in your turn handler. Since you are not awaiting the call, the turn will end and the bot will not hang up or send the timeout message. Here is what I did with a simulation I created.
await dc.context.sendActivity(`OK, I'll simulate a long-running API call and send a proactive message when it's done.`);
const conversationData = await this.dialogState.get(context, {});
apiSimulation.longRunningRequest(conversationData.conversationReference);
// That is in a switch statement. At the end of my turn handler I save state
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
And then the function that I called. As this was just a simulation, I have just awaited a promise, but obviously you would call and await your API(s). Once that comes back you will create a new BotFrameworkAdapter to send the proactive message back to the user.
const request = require('request-promise-native');
const { BotFrameworkAdapter } = require('botbuilder');
class apiSimulation {
static async longRunningRequest(conversationReference) {
console.log('Starting simulated API');
await new Promise(resolve => setTimeout(resolve, 30000));
console.log('Simulated API complete');
// Set up the adapter and send the message
try {
const adapter = new BotFrameworkAdapter({
appId: process.env.microsoftAppID,
appPassword: process.env.microsoftAppPassword,
channelService: process.env.ChannelService,
openIdMetadata: process.env.BotOpenIdMetadata
});
await adapter.continueConversation(conversationReference, async turnContext => {
await turnContext.sendActivity('This message was sent after a simulated long-running API');
});
} catch (error) {
//console.log('Bad Request. Please ensure your message contains the conversation reference and message text.');
console.log(error);
}
}
}
module.exports.apiSimulation = apiSimulation;

Related

Inserting ACL after creating Google Room Resource frequently throws error

I have a Google Cloud function which first creates a Google Room Resource using resources.calendars.insert method from the Google admin sdk,
and right after I try to insert an ACL using Acl: insert method from the google calendar api.
Similar to the following code:
const AdminService = google.admin({version: 'directory_v1'});
try {
const response = await AdminService.resources.calendars.insert(options); // options omitted
} catch (error) {
console.log(`Google Room Resource FAIL`);
console.error(error.message);
}
await new Promise((r) => setTimeout(r, 10000));
const CalendarService = google.calendar({version: 'v3'});
try {
const res = await CalendarService.acl.insert(option); // options omitted
console.log(res);
} catch (error) {
console.log(error);
throw new Error(error.message);
}
As for the authentication, I am using a service account with the correct scopes which impersionates an admin user with the correct permissions. This is how I generate the required JWT token:
const generateJWT = async (scope:string[])=>{
const jwtClient = new google.auth.JWT(
client_email, // service account
undefined,
private_key,
scope,
subject // admin user
);
return jwtClient;
}
In the options parameter for each api call I directly acquire the token for the auth attribute like this:
const option = {
'calendarId': acl.calendarId,
'auth': await generateJWT('https://www.googleapis.com/auth/calendar'),
'resource': {
'role': acl.role,
'scope': {
'type': acl.scopeType,
'value': acl.scopeValue,
},
},
};
Since I await all api calls, I thought that I will only get the response back when everything is already propagated in Google Workspace but when I do not use the setTimeout in between I always get an Error: Not Found back.
First I had the timeout set to 5 seconds which worked until it didn't so I moved it up to 10 seconds. This worked quite long but now I get again sometimes the Not Found error back...
I don't like the setTimeout hack...and even less if it does not work reliable, so how should I deal with this asynchronous behavior without spinning up any other infrastructure like queues or similar?
Working with Google Workspace Calendar Resource
As a Super Admin on my organization when creating a Calendar Resource, from the API or the Web interface, it could take up to 24 hours to correctly propagate the information of the Calendar for the organizations, which generally affect the time it would take for any application to gather the ID of the newly created calendar, which could explain why you are increasing the time out.
You have already implemented the await option which is one of the best things you can do. You can also review the option to apply exponential back off to your application or similar to Google App Script a Utitlies.sleep().
There are multiple articles and references on how to utilize it for the retry process needed when the Resource itself has not fully propagated correctly.
You can also review the official Calendar API documentation that suggests that the error "Not Found" is a 404 error:
https://developers.google.com/calendar/api/guides/errors#404_not_found
With a suggested action of reviewing the option to set up exponential backoff to the application.
References:
GASRetry - Exponential backoff JavaScript implementation for Google Apps Script
Exponential backoff

Telegram Bot Chain of Multiple Messages Sending NodeJS - Crashing

I am trying to send multiple messages to telegram bot through telegraf API in node backend. There are various conditions which when true, should send an telegram alert. The example of condition is:
if(condition is true) {
message = "Hey there";
sendToTGBot(message)
}
There are approximately 100 conditions which get checked within few seconds when the data is fetched as a stream from the API. So, the telegram bot limit gets hit when trying to process more than 30 messages in a second and the server crashes.
The code used to send messages from the bot is:
export const sendToTGBot = (alert) => {
bot.telegram.sendMessage(chat_id, alert);
}
I have also tried putting a delay using setTimeout() in the above code as:
export const sendToTGBot = (alert) => {
setTimeout(() => {
bot.telegram.sendMessage(chat_id, alert);
}, 500);
}
This code above puts the delay (I think so), but due to approximately 30-50 conditions getting true at once in a second. The function gets called that many times and after approx. 10-15 seconds all the alerts get sent simultaneously, again hitting the telegram bot limit.
Also, tried it like this:
export const sendToTGBot =
setTimeout((alert) => {
bot.telegram.sendMessage(chat_id, alert);
}, 500);
This results in a straight error.
Kindly help me with this problem. I have to send multiple alerts within a duration. The number of alerts simultaneously can increase. So, recommend me the best possible solution using JavaScript. Thanks.
You can do this by adding the messages in a queue/array, then utilize async await to loop it
let queue = [];
if(condition == true) {
message = "Hey there";
queue.push(message);
}
setInterval(async ()=>{
if(queue.length>0) {
await bot.telegram.sendMessage(chat_id, queue[0]);
queue.shift();
}
}, 1000)

Message Specific PFP. (like TupperBox)

I am looking to send a message that has a different profile picture than the bot. Basically, like Tupper Bot does. I can't seem to figure out how to do it anywhere, I've read the documentation and searched around.
You can use a webhook to do what you describe. If you already have a webhook link see here on how to use it. This works from external programs too.
If you don't, you can create a webhook with your bot and then send messages to it. In this example we will create a simple webhook and send a message to it.
First we should check if the channel already has a webhook. That way you don't create a new one everytime you use the command. You do that with the fetchWebhooks() function which returns a collection if there is a webhook present. If that is the case we will get the first webhook in the collection and use that. Note: if you have more then one webhook you can of course add additional checks after the initial one.
If there are no webhooks present, we create a new one with createWebhook(). You can pass additional options after the name.
let webhook;
let webhookCollection = await message.channel.fetchWebhooks();
if (webhookCollection.first()) {
webhook = webhookCollection.first();
} else {
webhook = await message.channel.createWebhook("new webhook", {
avatar: "https://i.imgur.com/tX5nlQ3.jpeg"
});
}
Note: in this example we use the channel the message came from but you can use any channel you want
Technically we can now send messages already.
webhook.send("this is a test");
What you can also do is create a webhookClient. Thats a little like the discord client you create when you start your bot. It takes the webhook ID and token as well as additional options you can set for clients.
const hookclient = new Discord.WebhookClient(webhook.id, webhook.token);
This lets you use a few additional methods like setInterval.
hookclient.setInterval(() => {
hookclient.send("This is a timer test");
}, 1000)
This lets you use the webhook without creating or fetching one first.

Referencing a DM channel

I'm trying to create a command that allows users to create their password following prompts through DM. I'm able to send a message to the user, but not able to read a message sent back with a MessageCollector because I cannot find a way to reference the DM channel.
I have tried using another instance of bot.on("message", message) here, but that creates a leak in the system causing the second instance to never disappear.
I also can't let users use a command say !CreatePassword *** because this function is linked with many others in a strict order.
Maybe I'm doing something fundamentally wrong, or approaching the problem in a bad way, but I need a way to reference a DM channel.
This is the best iteration of my code so far.
function createAccount(receivedMessage, embedMessage)
{
const chan = new Discord.DMChannel(bot, receivedMessage.author);
const msgCollector = new Discord.MessageCollector(chan , m => m.author.id == receivedMessage.author.id);
msgCollector.on("collect", (message) =>
{
// Other Code
msgCollector.stop();
// Removing an embed message on the server, which doesn't have a problem.
embedMessage.delete();
})
}
I can show the rest of my code if necessary.
Thank you for your time. I've lost an entire night of sleep over this.
I would do it like this (I'll assume that receivedMessage is the message that triggered the command, correct me if I'm wrong)
async function createAccount(receivedMessage, embedMessage) {
// first send a message to the user, so that you're sure that the DM channel exists.
let firstMsg = await receivedMessage.author.send("Type your password here");
let filter = () => true; // you don't need it, since it's a DM.
let collected = await firstMsg.channel.awaitMessages(filter, {
maxMatches: 1, // you only need one message
time: 60000 // the time you want it to run for
}).catch(console.log);
if (collected && collected.size > 0) {
let password = collected.first().content.split(' ')[0]; // grab the password
collected.forEach(msg => msg.delete()); // delete every collected message (and so the password)
await firstMsg.edit("Password saved!"); // edit the first message you sent
} else await firstMsg.edit("Command timed out :("); // no message has been received
firstMsg.delete(30000); // delete it after 30 seconds
}

How to read Client.postMessage before the page loaded?

I have a service worker that emits Client.postMessage during fetch when a cached resource has changed. I'm using this to notify the user that they might want to refresh.
My problem is that when the active page resource is changed and the service worker emits that message, the page hasn't loaded yet so no javascript can receive the message.
Is there a better way to handle cases like this rather than using waitUntil to pause a few seconds before emitting the message?
Another option would be to write to IndexedDB from the service worker, and then read it when the page loads for the first time, before you establish your message listener.
Using the ibd-keyval library for simplicity's sake, this could look like:
// In your service worker:
importScripts('https://unpkg.com/idb-keyval#2.3.0/idb-keyval.js');
async function notifyOfUpdates(urls) {
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({
// Structure your message however you'd like:
type: 'update',
urls,
});
}
// Read whatever's currently saved in IDB...
const updatedURLsInIDB = await idb.get('updated-urls') || [];
// ...append to the end of the list...
updatedURLsInIDB.push(...urls);
// ...and write the updated list to IDB.
await idb.set('updated-urls', updatedURLsInIDB);
}
// In your web page:
<script src="https://unpkg.com/idb-keyval#2.3.0/idb-keyval.js"></script>
<script>
async listenForUrlUpdates() {
const updatedURLsInIDB = await idb.get('updated-urls');
// Do something with updatedURLsInIDB...
// Clear out the list now that we've read it:
await idb.delete('updated-urls');
// Listen for ongoing updates:
navigator.serviceWorker.addEventListener('message', event => {
if (event.data.type === 'update') {
const updatedUrls = event.data.urls;
// Do something with updatedUrls
}
});
}
</script>

Categories