I am writing an app that features an inventory in which users can reserve products. I want to ensure that 2 users cannot simultaneously reserve a product at the same time, for this, I intend on using transactions. When using transactions from the Firebase SDK, everything works as intended, but I am getting unexpected behavior when using transactions from a callable cloud function. To simulate the use case where 2 users happen to reserve the same product, I use setTimeout in my cloud function to halt the function for 3 seconds. I am launching this function from 2 different clients with different user contexts.
export const reserveProduct = functions.https.onCall(async (data,context) => {
function testTimeout(){
return new Promise((resolve,reject) => {
setTimeout(()=> {
return resolve(true)
},3000)
})
}
if(!context.auth){
return {
error: `You must be logged in to reserve products`
}
}else{
const productRef = admin.firestore().collection('products').doc(data.productID)
const userRef = admin.firestore().collection('users').doc(context.auth.uid)
return admin.firestore().runTransaction((transaction) => {
return transaction.get(productRef).then(async(doc) => {
if(doc.get('status') == 'reserved'){
throw "Document already reserved!"
}else{
console.log("Product not reserved, reserving now!")
}
await testTimeout()
transaction.update(productRef, {status: 'reserved'});
transaction.update(userRef, {reserved: admin.firestore.FieldValue.arrayUnion(data.productID)})
})
}).then(() => {
console.log("Transaction Successfully committed !")
}).catch((error) => {
throw "Transaction failed, product already reserved"
})
}
After running this function call from 2 different clients simultaneously, The function call from my first client returns successfully as expected, but only after roughly 35s (which is way too long for the simplicity of the transaction). However, the second function call times out without returning any value. I have not seen any documentation explicitly stating the use of transactions in callable cloud functions, nor should it be affected when used within the emulator.
I am expecting to simply get a return value for whichever function call is able to modify the data first, and catch the error from the function which has retried and validated the reserved state.
Any help would be appreciated, thanks!
One major difference between the two places is in the way the SDKs used handle transactions:
The client-side SDKs use an optimistic compare-and-set approach for transactions, meaning that they pass the values you read in the transaction with the data you're writing. The server then only writes the new data if the documents you read haven't been updated.
The server-side SDKs (used in your Cloud Function) use a more traditional pessimistic approach for transactions, and place a lock on each document that you read in the transaction.
You can read more about database contention in the SDKs in the documentation.
While I'm not exactly certain how this is affecting your code, I suspect it is relevant to the difference in behavior you're seeing between the client-side and server-side implementations.
I have updated my Firebase Realtime Database access rules, and have noticed some clients now tries to access paths they do not have access to. This is ok - but my problem is that my code stops after being unable to read a restricted node.
I see below error in my console, and then loading of subsequent data stops:
permission_denied at /notes/no-access-node
I begin by collecting access nodes from /access_notes/uid and continue to read all data from /notes/noteId.
My code for collecting notes from the database below:
//*** SUBSCRIPTION */
database.ref(`access_notes/${uid}`).on('value', (myNotAccessSnaps) => {
let subscrPromises = []
let collectedNots = {}
// Collect all categories we have access to
myNotAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
subscrPromises.push(
database.ref(`notes/${noteId}`)
.once('value', (notSnap)=>{
const notData = notSnap.val()
const note = { id: notSnap.key, ...notData}
collectedNotes[note.id] = note
},
(error) => {
console.warn('Note does not exist or no access', error)
})
)
})
Promise.all(subscrPromises)
.then(() => {
const notesArray = Object.values(collectedNotes)
...
})
.catch((error) => { console.error(error); return Promise.resolve(true) })
I do not want the client to halt on permission_denied!
Is there a way to see if the user has access to a node /notes/no_access_note without raising an error?
Kind regards /K
I do not want the client to halt on permission_denied!
You're using Promise.all, which MDN documents as:
Promise.all() will reject immediately upon any of the input promises rejecting.
You may want to look at Promise.allSettled(), which MDN documents as:
[Promise.allSettled()] is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, or you'd always like to know the result of each promise.
Is there a way to see if the user has access to a node /notes/no_access_note without raising an error?
As far as I know the SDK always logs data access permissions errors and this cannot be suppressed.
Trying to access data that the user doesn't have access to is considered a programming error in Firebase. In normal operation you code should ensure that it never encounters such an error.
This means that your data access should follow the happy path of accessing data it knows it has access to. So you store the list of the notes the user has access to, and then from that list access each individual note.
So in your situation I'd recommend finding out why you're trying to read a note the user doesn't have access to, instead of trying to hide the message from the console.
I'm currently trying to create a Discord bot that sends a message when a user with a specified role sends the command %sticky This is a test
I want it to always be the first message in the channel and every time another user types in the channel the bot deletes its last message and posts again. I haven't had any luck online yet even finding a bot that already does this functionality, or where to even start. Here's what I kind of have so far
var lastStickyMessage;
client.on('message', message => {
if (lastStickyMessage != null) {
message.channel.fetchMessage(lastStickyMessage)
.then(retrievedMessage => retrievedMessage.delete());
}
message.reply("This is a Sticky Message").then(sent => {
let lastStickyMessage = sent.id;
}
});
There are several errors with your variable management: One one hand you create a new let with the same name. Since a let is a scoped variable, the lastStickyMessage will have a different value inside the sent callback than it has outside of it, since those are two different variables (read more on this here).
Apart from that you should save the last sent ID in a file or somewhere since the var will be reset once you restart your bot (the built in fs module could help you with that, you can find the documentation here).
One last thing: If you initialize a variable without a value it is not null but undefined. If you only check using == it will still evaluate to true (means null == undefined) but if you compare using ===, it will evaluate to false (null !== undefined). In your case this is not really a problem but this might be good to know for other cases.
I am building a Slack app using the JavaScript Bolt framework. The concept of the app is just listening to specific message keywords in channels and then forwarding those messages to the users of the app.
What I am trying to achieve is including a permalink in the forwarded message. I am trying to use the chat.getPermalink method to get the url and then include that in my chat.postMessage method. I am trying to leverage Bolt's 'Context' in order to pass the property in chat.getPermalink to chat.postMessage. I am asking for help here because I cannot get the Context to work..
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
let token = process.env.SLACK_BOT_TOKEN,
web = new WebClient(token);
let jira_text = "jira";
let rdu_qa = '#rdu_qa';
//Get permalink
async function PermaLinks({payload, context, next}) {
let perm = app.client.chat.getPermalink({
token: context.botToken,
channel: "C0109KMQCFQ",
message_ts: payload.ts
});
context.permalink = perm.permalink;
await next();
}
app.event('message', PermaLinks, async ({ payload, message, context}) => {
let userzArray = ["D010Q34TQL9", "UVBBD8989"];
//if channel is general and incldues the text 'Jira' or 'rdu_qa'
if (payload.channel === "C0109KMQCFQ") {
if (payload.text.includes(jira_text) || payload.text.includes(rdu_qa)) {
try {
// Call the chat.postMessage to each of the users
let oneUser = await userzArray.forEach(userId => { app.client.chat.postMessage({
token: context.botToken,
bot_id: "USLACKBOT",
channel: userId,
blocks: [
{
type: "section",
text: {
text: payload.text,
type: "mrkdwn"
},
fields: [
{
type: "mrkdwn",
text: `posted by <#${message.user}>`
},
{
type:"mrkdwn",
text: "in General channel" //channel.name//getChannelNameGeneral
},
{
type:"mrkdwn",
text: context.permalink // Permalink should be right here
}
]
},
{
"type": "divider"
},
] // End of block of Jira notification stuff
});
});
// console.log(result);
} catch (error) {
console.error(error);
}
} // If text sent to General channel includes keyword 'Jira' or 'rdu_qa'
} //end of if message was posted in General channel
There are a couple problems I can see in the example code, but I think the main issue regarding the context is that you're storing a Promise as context.permalink, not the actual result of the method call. In order to store the result, you should use the await keyword before calling the method (app.client.chat.getPermalink(...)).
I've revised the code you shared here, and I'll explain the modifications below.
const { App } = require('#slack/bolt');
const token = process.env.SLACK_BOT_TOKEN
const app = new App({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token,
});
// Users who should be notified when certain messages are heard
let userzArray = ["D010Q34TQL9", "UVBBD8989"];
// Conversation IDs corresponding to the users in the array above. This variable will be set automatically when the app starts.
let conversationsToNotify;
// Match messages that include the text 'jira' or '#rdu_qa'
app.message(/jira|#rdu_qa/, async ({ message, client }) => {
// Match the messages that are in the specified channel
if (message.channel === 'C0109KMQCFQ') {
try {
// Get a permalink to this message
const permalinkResult = await client.chat.getPermalink({
channel: message.channel,
message_ts: message.ts,
});
// Send a message to each user containing the permalink for this message
await Promise.all(conversationsToNotify.map((conversationId) => {
return client.chat.postMessage({
channel: conversationId,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `>>> ${payload.text}`,
},
fields: [
{
type: 'mrkdwn',
text: `posted by <#${message.user}>`,
},
{
type: 'mrkdwn',
text: `in <#${message.channel}>`,
},
{
type:'mrkdwn',
text: `<Original|${permalinkResult.permalink}>`,
},
],
},
{
type: 'divider'
},
],
});
}));
} catch (error) {
console.error(error);
}
}
});
async function convertUsersToConversations(input) {
return Promise.all(input.map((id) => {
// For all IDs that seem like user IDs, convert them to a DM conversation ID
if (id.startsWith('U')) {
return app.client.conversations.open({
token,
users: id,
})
.then((result) => result.channel.id);
}
// For all IDs that don't seem to belong to a user, return them as is
return id;
}));
});
(async () => {
// Start the app
conversationsToNotify = await convertUsersToConversations(userzArray);
await app.start(process.env.PORT || 3000);
console.log('⚡️ Bolt app is running!');
})();
I've removed the initialization of a new WebClient object. In Bolt v1.6.0 and later, there is a client argument available in listeners and middleware which you can use to call Web API methods instead. The advantage of using the client argument is that you don't need to read the token from the context and pass it as an argument for each method call on your own, Bolt will find the right token for you.
Instead of using the app.event('message', ...) method to listen for message events, I've changed to using app.message(...). The latter works mostly the same, but has one more advantage: you can pass a pattern to match the text of a message as the first argument (before the listener function): app.message(pattern, ...). That helps remove some of the conditions inside the listener. Instead of using just the two string variables jira_text and #rdu_qa, I've combined them in a single regular expression that matches when either of those values is seen in the text: /jira|#rdu_qa/.
Instead of using middleware to find the permalink of a message, I've moved that code into the listener. Middleware should be used to reuse code across multiple listeners (or global middleware to reuse code across all listeners). In your example, it doesn't seem like the code to find the permalink is being reused, but if you do use this in many listeners, it should be relatively easy to extract. Another advantage is now the logic only runs after the pattern was matched, so you're not making these calls for every single message that the bot sees in all channels that it is a member of (this is much better for performance).
Use Promise.all() to collect the Promises of each call to chat.postMessage into one promise. Currently, you're using userzArray.forEach(...), which doesn't return anything. So then using await on that value will immediately resolve, and doesn't really do anything useful. What we need to do is collect each of the Promises and wait for them to all complete. This is what Promise.all() does. We just need an array of Promises to pass in, which we can get by simply changing userzArray.forEach() to userzArray.map().
There's a problem with the way you're calling chat.postMessage. You're trying to use Slackbot to send those messages, but that's not recommended because users are less likely to understand where that message is coming from. Instead, you should send this message as a DM from your bot user. In order to do that, you need a conversation ID, not a user ID, for each user you want to send this notification to. One of the items in userzArray is already a DM conversation ID (it starts with a D), but the other is not. In order to make this work consistently, I've created the conversationsToNotify array which contains the conversation IDs for each user after calling conversations.open to create a DM. Now in the code, you'll see conversationsToNotify.map() instead of userzArray.map(). Your Slack app will now need the im:write and chat:write permission scopes (don't forget to reinstall once you add scopes). Looking up the conversation IDs will slow down your app from starting up if the number of users in the array gets larger. My recommendation would be to save the conversation IDs in your code (or in a database) instead of the user IDs. This will ensure a consistently fast start up time for your app.
There's an opportunity to do even better. What happens when the first call to chat.postMessage fails? The way I've written the code above, the error would be logged to the console, but later if the second call fails, there's no way to know. That's because Promise.all() returns a promise that will reject as soon as any one of the promises rejects, and then ignores what happens afterwards. If you're using Node v12.9.0 or greater, I would recommend using Promise.allSettled() instead (which would require a few changes in your catch clause as well).
General cleanup:
Use message argument in the listener everywhere instead of payload argument. These are actually the same value when dealing with message events. payload is mostly only useful in middleware that handle several kinds of events (action, event, view, shortcut, etc) so that there's one way to refer to all of their payloads.
Move userzArray outside the listener, and make it a constant. There's no point in redeclaring it inside the listener each time it runs, and it doesn't change.
I added a function to convert from user IDs to conversation IDs (convertUsersToConversations). This function is called before the app is started to avoid a race condition where the an incoming message is handled before the app knows which channels to notify.
Formatted the text content of the message as quoted text, formatted the channel mention, and formatted the permalink. One improvement I'd also recommend is to use a context block to show the message author's name and avatar image.
I just wanted to know if there's a way to count how many times a message has been sent in my Discord server, so the bot can send a message. I'm new with coding, so I don't know many things. Thank you in advance!
Explanation
To store the amount of messages sent in a guild, you'll have to keep track of a count somehow. Each time a message is sent, you can increment it by 1. Then, upon a user's request, you can display that number.
One easy option would be to store this "message count" for each guild inside of a JSON file. However, this would greatly impact performance. Consider a database for much better speeds and reliability.
Example Setup
Before using this system, create a guilds.json file with a blank object ({}).
Declaring the necessary variables...
const fs = require('fs'); // fs is the built-in Node.js file system module.
const guilds = require('./guilds.json'); // This path may vary.
Adding the system to the message event listener...
client.on('message', message => {
// If the author is NOT a bot...
if (!message.author.bot) {
// If the guild isn't in the JSON file yet, set it up.
if (!guilds[message.guild.id]) guilds[message.guild.id] = { messageCount: 1 };
// Otherwise, add one to the guild's message count.
else guilds[message.guild.id].messageCount++;
// Write the data back to the JSON file, logging any errors to the console.
try {
fs.writeFileSync('./guilds.json', JSON.stringify(guilds)); // Again, path may vary.
} catch(err) {
console.error(err);
}
}
});
Using the system in a command...
// Grab the message count.
const messageCount = guilds[message.guild.id].messageCount;
// Send the message count in a message. The template literal (${}) adds an 's' if needed.
message.channel.send(`**${messageCount}** message${messageCount !== 1 ? 's' : ''} sent.`)
.catch(console.error);
JSON is highly prone to corruption if a queue system is not created that will make sure multiple reads and writes are not happening to a file all at the same time. For the purpose of what you want, I would use something like SQLite that requires minimal setup, is easy to learn, and has helper frameworks to make it easier to use such as Keyv and Sequelize.
Here is a good guide on how to use sqlite in the nodejs runtime environment.