delete incoming write event after calculations in firebase functions - javascript

I have an app that uses firebase, the whole stack pretty much, functions, database, storage, auth, messaging, the whole 9. I want to keep the client end very lightweight. So if a user comments on a post and "tags" another user, let's say using the typical "#username" style tagging, I moved all of the heavy lifting to the firebase functions. That way the client doesn't have to figure out the user ID based on the username, and do everything else. It is setup using triggers, so when the above scenario happens I write to a "table" called "create_notifications" with some data like
{
type: "comment",
post_id: postID,
from: user.getUid(),
comment_id: newCommentKey,
to: taggedUser
}
Where the taggedUser is the username, the postID is the active post, the newCommentKey is retrieved from .push() on the comments db reference, and the user.getUid() is from the firebase auth class.
Now in my firebase functions I have a "onWrite" trigger for that specific table that gets all of the relevant information and sends out a notification to the poster of the post with all the relevant details. All of that is complete, what I am trying to figure out is... how do I delete the incoming event, that way I don't need any sort of cron jobs to clear out this table. I can just grab the event, do my needed calculations and data gathering, send the message, then delete the incoming event so it never even really exists in the database except for the small amount of time it took to gather the data.
A simplified sample of the firebase functions trigger is...
exports.createNotification = functions.database.ref("/create_notifications/{notification_id}").onWrite(event => {
const from = event.data.val().from;
const toName = event.data.val().to;
const notificationType = event.data.val().type;
const post_id = event.data.val().post_id;
var comment_id, commentReference;
if(notificationType == "comment") {
comment_id = event.data.val().comment_id;
}
const toUser = admin.database().ref(`users`).orderByChild("username").equalTo(toName).once('value');
const fromUser = admin.database().ref(`/users/${from}`).once('value');
const referencePost = admin.database().ref(`posts/${post_id}`).once('value');
return Promise.all([toUser, fromUser, referencePost]).then(results => {
const toUserRef = results[0];
const fromUserRef = results[1];
const postRef = results[2];
var newNotification = {
type: notificationType,
post_id: post_id,
from: from,
sent: false,
create_on: Date.now()
}
if(notificationType == "comment") {
newNotification.comment_id = comment_id;
}
return admin.database().ref(`/user_notifications/${toUserRef.key}`).push().set(newNotification).then(() => {
//NEED TO DELETE THE INCOMING "event" HERE TO KEEP DB CLEAN
});
})
}
So in that function in the final "return" of it, after it writes the finalized data to the "/user_notifications" table, I need to delete the event that started the whole thing. Does anyone know how to do that? Thank you.

First off, use .onCreate instead of .onWrite. You only need to read each child when they are first written, so this will avoid undesirable side effects. See the documentation here for more information on the available triggers.
event.data.ref() holds the reference where the event occurred. You can call remove() on the reference to delete it:
return event.data.ref().remove()

The simplest way to achieve this is through calling the remove() function offered by the admin sdk,
you could get the reference to the notification_id through the event, i.e event.params.notification_id then remove it when need be with admin.database().ref('pass in the path').remove(); and you are good to go.

For newer versions of Firebase, use:
return change.after.ref.remove()

Related

How to check if a container exists in cosmos DB using the node sdk?

I want to check if a container exists and if not, initialize it. I was hoping for something like the following:
const { endpoint, key, databaseId } = config;
const containerName = "container1"
const client = new CosmosClient({ endpoint ,key});
const containerDefinition = getContainerDefinition(containerName);
const db = await createDatabase(client, databaseId);
if (!db.containers.contains(containerName)
{
// Do something
}
The reason I'm not using "createIfNotExists" is because I would need to make a 2nd call to check if the container returned is populated with items or not. The container I'm creating is going to hold settings data which will be static once the container is initially created. This settings check is going to happen per request so I'd like to minimize the database calls and operations if possible.
I tried doing something like:
try
{
db.container(containerName).read();
}
catch(err)
{
if(err.message.contains("Resource Not Found"))
{
// do something
}
}
But that doesn't seem like the right way to do it.
Any help would be appreciated!
I'm not quite clear on why you would need to do this since typically you only need to do this sort of thing once for the life of your application instance. But I would not recommend doing it this way.
When you query Cosmos to test the existence of a database, container, etc., this hits the master partition for the account. The master partition is kind of like a tiny Cosmos database with all of your account meta data in it.
This master partition is allocated a small amount of the RU/s that manage the metadata operations. So if you app is designed to make these types of calls for every single request, it's quite likely you will get rate limited in your application.
If there is some way you can design this such that it doesn't have to query for the existence of a container then I would pursue that instead.
Interesting question. So i think you have few options
Just call const { container } = await database.containers.createIfNotExists({ id: "Container" }); it will be fast probably few milliseconds, since I went via code at looks like it will always try to read from cosmos :( If you want to still check if container exists sdk has methods(But again no real benefits ):
const iterator = database.containers.readAll();
const { resources: containersList } = await iterator.fetchAll();
Create singleton and first time just initialise all your containers so next time you dont call it, sure if you scale each instance will do the same
My favourite, use terraform/armtemplates/bicep to spin up infrastructure so you code wont need to handle that
You can try this code:
async function check_container_exist(databaseId,containerId) {
let exist = false;
const querySpec = {
query: "SELECT * FROM root r WHERE r.id = #container",
parameters: [
{name: "#container", value: containerId}
]
};
const response = await client.database(databaseId).containers.query(querySpec).fetchNext();
if(response.resources[0]){
exist = true;
}
return exist;
}

(Slack API Bolt project) Using Context object to pass property from one method to another

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.

How to query database with firebase cloud functions

I am trying to query my firestore database using cloud functions.
I want to trigger an email notification every time a new reading in my database is under the value of 10.
Here is the relevant database structure for reference: database structure.
The "readings" field is an array and each "reading" is a map which holds the fields "date" and "value".
Currently I am at the point where I can send an email notification every time a new user is created however I want this to work for the database. I am unsure how to query for the "readings" array and then for each individual reading.
Here is my code so far which sends an email when a new user is created
exports.sendNotification = functions.auth.user().onCreate((user) => {
const mailOptions = {
from: '"Spammy Corp." <noreply#firebase.com>',
to:"fakeEmail#btopenworld.com",
text: "TEST"
};
return mailTransport.sendMail(mailOptions)
.then(() => console.log("It worked"))
.catch((error) =>
console.error('There was an error while sending the email:', error));
});
See: https://firebase.google.com/docs/firestore/extend-with-functions
For example, to fire on all new readings added to that first child:
exports.sendEmail = functions.firestore
.document('sensor/UGt.../readings')
.onCreate((snap, context) => {
const newValue = snap.data();
const value = newValue.value;
if (value < 10) {
// send email
}
});
In further comments you mentioned listening for new readings in all sensor elements, not just your first one. This is unfortunately not possible in an efficient / simple way (source). Instead you will have to listen to all onUpdate events on /sensor/, check if the update is adding a reading, then check the value & send your email.
It may be easier to call the cloud function directly from wherever adds the reading, depending on how many times the /sensor/ path is going to be updated for other reasons (since every time this happens, it's a waste of resources).

How can I make Dialogflow agent greet user if they have used the action before?

I'm using Actions On Google / Dialogflow, and I'm trying to make a function that will greet a user by their name if they've used the action before, and if not, will ask for their name. I have tried to map this to the "Welcome intent" through fulfillment, but whenever I try to run the action on the simulator, I get this error:
Error 206: Webhook Error
Which Initially would make sense if this was mapped to another intent, but I'm wondering if I'm getting this error because you can't have a fulfillment with the welcome intent?
Here's the code I'm using in the inline editor which may be the problem:
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request,response) => {
const agent = new WebhookClient({ request, response });
function welcome(conv) {
if (conv.user.last.seen) {
conv.ask(`Welcome back ${name}!`);
} else {
conv.ask('Welcome to The app! My name is Atlas, could I get your name?');
}}
let intentMap = new Map();
intentMap.set('Welcome Intent', welcome);
agent.handleRequest(intentMap);
How come this isn't working? Do I need to implement user login? Do I need to use a function that would write to a firestore databbase?
Thanks for the help or suggestions!
Let's clear a few things up to start:
You can have fulfillment with your welcome intent.
You do not need user login. Although using Google Sign In for Assistant can certainly be used, it doesn't fundamentally change your problem here.
You do not need to use a function that writes to the firestore database. Again, you could use it, but this doesn't change your problems.
The specific reason this isn't working is because the conv parameter in this case contains a Dialogflow WebhookClient rather than an actions-on-google Conversation object.
To get the Conversation object with the parameter you have, you can call conv.getConv(), which will give you an object that has a user parameter. So this may look something like
function welcome(conv) {
let aog = conv.getConv();
if (aog.user.last.seen) {
conv.ask(`Welcome back ${name}!`);
} else {
conv.ask('Welcome to The app! My name is Atlas, could I get your name?');
}}
There are, however, still some issues with this. Most notably, it isn't clear where name will come from. I assume you will get it out of the user store object, but you don't seem to have done this.
For anyone who comes across this question in the future and just wants a straight forward answer without having to search through ambiguous answers / documentation, here is what to do step by step:
note: I ended up using the Google Sign in method, but even if this isn't your goal, i'll post the link to the alternative method.
1) Import the actions on google module. What people / tutorials don't to show is you have to import the library like this (for user login):
const {
dialogflow,
Permission,
SignIn
} = require('actions-on-google')
instead of
const dialogflow = require('actions-on-google')
2) Use this code:
const app = dialogflow({
clientId: '<YOUR CLIENT ID from Actions on Google>',
});
app.intent('Start Signin', conv => {
conv.ask(new SignIn('To get your account details'));
});
app.intent('Get Signin', (conv, params, signin) => {
if (signin.status === 'OK') {
const payload = conv.user.profile.payload;
conv.ask(`Welcome back ${payload.name}. What do you want to do next?`);
} else {
conv.ask(`I won't be able to save your data, but what do you want to do next?`);
}
});
This function will ask the user for a login, and next time you invoke the intent, it will say "Welcome back name", because google automatically saves it.
Here's the link to the alternative method:

mongodb getting user info for every document

I'm trying to display a forum/category. I need to get the latest posts. The problem is that I also need data on the user for each post as well as the latest reply.
db.post.find({
"inForum": forumID,
},
{
'sort': [['date', -1]]
},
function(err, cursor) {
cursor.count(function(err, count) {
cursor.skip(skip).limit(20).toArray(function(err, posts) {
var complete = _.after(nodes.length, function () {
res.send(posts)
});
// for every post get its author info and the latest post info
posts.forEach(function (post) {
var users = _.pluck(posts, 'user');
user.load(users, function (profiles) {
_.each(posts,
function(post, k) {
if (profiles[post.user]) post.fieldAvatar = profiles[post.user].fieldAvatar;
});
if (post.latestReply) {
post.load(post.latestReply.id, function (latestReply) {
if (latestReply) post.latestReply = latestReply
complete()
})
}
else {
complete()
}
})
})
});
})
})
This is what I'm doing and it seems really slow / really inelegant to me. Am I doing this correctly and is there any advice for speeding this up?
Thanks.
The best thing you should do here is to embed some information for the author of the posts (username & email or avatar) into those posts so that you don't make multiple queries to the database, one should suffice (sure you have some duplicate data, but the performance is optimal).
If you don't want to / can't do that you can also modify your second query to find all authors in [array_of_ids_of_the_posts]. That would reduce your [number_of_posts] queries into only one.
You could use some caching. For example you could save the users in an dictionary during the loop so you only have to fetch it on the first occurrence from mongodb.
Maybe you could create some kind of thread model where you save basic information about the containing posts, so you only have to go through the threads.
You could save the result of the function and delete it when a new post is added .. so won't go through all posts on every call.
You should not use a document storage like a sql database. Maybe it is better to generate the forum page directly when a post is created/edited and save the whole data in a document, so you only have to make one read call to mongo to show it.

Categories

Resources