I am new at using Firebase so all advice is welcome.
What i'm trying to achieve?
I want to create a player only if the user has not exceeded their team size ("numberOfplayersLimit").
So currently I am using a firebase transaction which first checks that the team has not exceeded their limit "numberOfPlayers", if the team has not exceeded their limit, increment the "numberOfplayersLimit" counter and then add the player to the database as shown below.
Whats my issue
I am currently using .push() to add the players however it is creating the player twice as shown below as you can see the full name is the same in other records but they have different uids.
Below is a screenshot from my Firebase real-time database JSON structure
var myUserId = firebase.auth().currentUser.uid;
const playerData = {
fullName:this.state.fullName,
};
//This is where the players are stored
const teamplayersref = firebase.database().ref('/teams').child(myUserId).child('/players')
//Transaction - Team reference path for the TeamPlayers Limit
const getTeamPlayersLimit = firebase.database().ref('/teams').child(myUserId).child('numberOfplayersLimit');
getTeamPlayersLimit.transaction(function(numberOfplayersLimit){
if (numberOfplayersLimit == 11) {
alert('You have exceed your team size limit, Delete a player from your team or contact us to upgrade your package');
}
else
{
//increment teamplayers limit
numberOfplayersLimit = numberOfplayersLimit + 1;
teamplayersref.push(playerData);
return numberOfplayersLimit;
}
});
The teamplayersref.push(playerData) call in your transaction handler is not part of the transaction itself. So if the transaction is retried, you end up calling teamplayersref.push(playerData) multiple times, creating a child node for each try.
To generate a new child with unique push ID, use push without an argument to get a new key, and then use that in the return value of your transaction. This means that your transaction will have to run on the entire firebase.database().ref('/teams') node, since you're modifying both the counter and the players.
const teamRef = firebase.database().ref('/teams').child(myUserId);
teamRef.transaction(function(team){
team = team || { numberOfplayersLimit: 0, players: {} };
if (team.numberOfplayersLimit == 11) {
console.error('You have exceed your team size limit, Delete a player from your team or contact us to upgrade your package');
}
else {
team.numberOfplayersLimit = team.numberOfplayersLimit + 1;
const newPlayerKey = teamRef.push().key; // this line does not write to the database
team.players[newPlayerKey] = playerData;
return team;
}
});
I am using TranscriptLoggerMiddleware and CosmosDB to log my chatbot transcripts. We are trying to capture the user state information (user name, account number, account type, etc) as top level attributes in the transcript so that specific customers can easily be queried in the DB (if that information is just in the individual timestamp attributes of the document, they can't be queried).
Ideally I would just add the user state when I'm building the file, but I can't figure any way to access it since the logger is defined in index.js and TranscriptLoggerMiddleware only provides the activity to my function, not the full context. If anyone has a way to get the user state data via TranscriptLoggerMiddleware, let me know, that would solve this issue. Here is the customLogger code. Note that due to the function receiving both the user query and bot response, I couldn't get retrieving and resaving the transcript to work, so I'm overwriting the transcript from a local log object. Not trying to come up with a new approach here but if one would solve the overall issue I'd like to hear it.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { CosmosDbPartitionedStorage } = require('botbuilder-azure');
const path = require('path');
/**
* CustomLogger, takes in an activity and saves it for the duration of the conversation, writing to an emulator compatible transcript file in the transcriptsPath folder.
*/
class CustomLogger {
/**
* Log an activity to the log file.
* #param activity Activity being logged.
*/
// Set up Cosmos Storage
constructor(appInsightsClient) {
this.transcriptStorage = new CosmosDbPartitionedStorage({
cosmosDbEndpoint: process.env.COSMOS_SERVICE_ENDPOINT,
authKey: process.env.COSMOS_AUTH_KEY,
databaseId: process.env.DATABASE,
containerId: 'bot-transcripts'
});
this.conversationLogger = {};
this.appInsightsClient = appInsightsClient;
this.msDelay = 250;
}
async logActivity(activity) {
if (!activity) {
throw new Error('Activity is required.');
}
// Log only if this is type message
if (activity.type === 'message') {
if (activity.attachments) {
try {
var logTextDb = `${activity.from.name}: ${activity.attachments[0].content.text}`;
} catch (err) {
var logTextDb = `${activity.from.name}: ${activity.text}`;
}
} else {
var logTextDb = `${activity.from.name}: ${activity.text}`;
}
if (activity.conversation) {
var id = activity.conversation.id;
if (id.indexOf('|') !== -1) {
id = activity.conversation.id.replace(/\|.*/, '');
}
// Get today's date for datestamp
var currentDate = new Date();
var day = currentDate.getDate();
var month = currentDate.getMonth()+1;
var year = currentDate.getFullYear();
var datestamp = year + '-' + month + '-' + day;
var fileName = `${datestamp}_${id}`;
var timestamp = Math.floor(Date.now()/1);
// CosmosDB logging (JK)
if (!(fileName in this.conversationLogger)) {
this.conversationLogger[fileName] = {};
this.conversationLogger[fileName]['userData'] = {};
this.conversationLogger[fileName]['botName'] = process.env.BOTNAME;
}
this.conversationLogger[fileName][timestamp] = logTextDb;
let updateObj = {
[fileName]:{
...this.conversationLogger[fileName]
}
}
// Add delay to ensure messages logged sequentially
await this.wait(this.msDelay);
try {
let result = await this.transcriptStorage.write(updateObj);
} catch(err) {
console.log(err);
this.appInsightsClient.trackTrace({message: `Logger Error ${err.code} - ${path.basename(__filename)}`,severity: 3,properties: {'botName': process.env.BOTNAME, 'error':err.body}});
}
}
}
}
async wait(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds) {
break;
}
}
}
}
exports.CustomLogger = CustomLogger;
Not being able to get user state in this function, I decided to try a few other approaches. The most promising was creating a separate "updateTranscript" function to grab the transcript, add user state, and save it back. But I think it was catching it only on user request and getting overidden again by local object on bot response. I added a delay to try to combat this, but it still didn't work. On my very first prompt of providing customer number user state data is getting stored on transcript, but at the next activity it is gone and never comes back (even though I can see it is supposedly getting written to DB). Here is that update function.
const { CosmosDbStorage } = require('botbuilder-azure');
var updateTranscript = async (context, userData, appInsightsClient) => {
const transcriptStorage = new CosmosDbStorage({
serviceEndpoint: process.env.COSMOS_SERVICE_ENDPOINT,
authKey: process.env.COSMOS_AUTH_KEY,
databaseId: process.env.DATABASE,
collectionId: 'bot-transcripts',
partitionKey: process.env.BOTNAME
});
var id = context.activity.conversation.id;
if (id.indexOf('|') !== -1) {
id = context.activity.conversation.id.replace(/\|.*/, '');
}
// Get today's date for datestamp
var currentDate = new Date();
var day = currentDate.getDate();
var month = currentDate.getMonth()+1;
var year = currentDate.getFullYear();
var datestamp = year + '-' + month + '-' + day;
var filename = `${datestamp}_${id}`;
var msDelay = 500;
await new Promise(resolve => setTimeout(resolve, msDelay));
var transcript = await transcriptStorage.read([filename]);
transcript[filename]['userData'] = userData
try {
await transcriptStorage.write(transcript);
console.log('User data added to transcript');
} catch(err) {
console.log(err);
appInsightsClient.trackTrace({message: `Log Updater Error ${err.code} - ${path.basename(__filename)}`,severity: 3,properties: {'botName': process.env.BOTNAME, 'error':err.body}});
}
return;
}
module.exports.updateTranscript = updateTranscript
I realize this approach is a bit of a cluster but I've been unable to find anything better. I know the Microsoft COVID-19 bot has a really nice transcript retrieval function, but I haven't been able to get any input from them on how that was accomplished. That aside, I'm quite happy to continue with this implementation if someone can help me figure out how to get that user state into the transcript without being overwritten or running into concurrency issues.
As to why I can't query an account number even via substring() function, here's an example of the documents data object. I have no idea which string to check for a substring, in this case 122809. I don't know what that timestamp could be. If this is stored at the top level (e.g. userData/accountNumber) I know exactly where to look for the value. For further context, I've displayed what I see after the first prompt for account number, where userData is populated. But it gets overidden on subsequent writes and I can't seem to get it back even with a delay in my updateTranscript function.
"document": {
"userData": {},
"botName": "AveryCreek_OEM_CSC_Bot_QA",
"1594745997562": "AveryCreek_OEM_CSC_Bot_QA: Hi! I'm the OEM CSC Support Bot! Before we get started, can you please provide me with your 6-digit Vista number? If you don't have one, just type \"Skip\".",
"1594746003973": "You: 122809",
"1594746004241": "AveryCreek_OEM_CSC_Bot_QA: Thank you. What can I help you with today? \r\nYou can say **Menu** for a list of common commands, **Help** for chatbot tips, or choose one of the frequent actions below. \r\n \r\n I'm still being tested, so please use our [Feedback Form](https://forms.office.com/Pages/ResponsePage.aspx?id=lVxS1ga5GkO5Jum1G6Q8xHnUJxcBMMdAqVUeyOmrhgBUNFI3VEhMU1laV1YwMUdFTkhYVzcwWk9DMiQlQCN0PWcu) to let us know how well I'm doing and how I can be improved!",
"1594746011384": "You: what is my account number?",
"1594746011652": "AveryCreek_OEM_CSC_Bot_QA: Here is the informaiton I have stored: \n \n**Account Number:** 122809 \n\n I will forget everything except your account number after the end of this conversation.",
"1594746011920": "AveryCreek_OEM_CSC_Bot_QA: I can clear your information if you don't want me to store it or if you want to reneter it. Would you like me to clear your information now?",
"1594746016034": "You: no",
"1594746016301": "AveryCreek_OEM_CSC_Bot_QA: OK, I won't clear your information. You can ask again at any time."
},
"document": {
"userData": {
"accountNumber": "122809"
},
"botName": "AveryCreek_OEM_CSC_Bot_QA",
"1594746019952": "AveryCreek_OEM_CSC_Bot_QA: Hi! I'm the OEM CSC Support Bot! What can I help you with today? \r\nYou can say **Menu** for a list of common commands, **Help** for chatbot tips, or choose one of the frequent actions below. \r\n \r\n I'm still being tested, so please use our [Feedback Form](https://forms.office.com/Pages/ResponsePage.aspx?id=lVxS1ga5GkO5Jum1G6Q8xHnUJxcBMMdAqVUeyOmrhgBUNFI3VEhMU1laV1YwMUdFTkhYVzcwWk9DMiQlQCN0PWcu) to let us know how well I'm doing and how I can be improved!"
},
You had said you were encountering concurrency issues even though JavaScript is single-threaded. As strange as that sounds, I think you're right on some level. TranscriptLoggerMiddleware does have its own buffer that it uses to store activities throughout the turn and then it tries to log all of them all at once. It could easily have provided a way to get that whole buffer in your own logger function, but instead it just loops through the buffer so that you still only get to log them each individually. Also, it allows logActivity to return a promise but it never awaits it, so each activity will get logged "simultaneously" (it's not really simultaneous but the code will likely jump between function calls before waiting for them to complete). This is a problem for any operation that isn't atomic, because you'll be modifying state without knowing about its latest modifications.
while (transcript.length > 0) {
try {
const activity: Activity = transcript.shift();
// If the implementation of this.logger.logActivity() is asynchronous, we don't
// await it as to not block processing of activities.
// Because TranscriptLogger.logActivity() returns void or Promise<void>, we capture
// the result and see if it is a Promise.
const logActivityResult = this.logger.logActivity(activity);
// If this.logger.logActivity() returns a Promise, a catch is added in case there
// is no innate error handling in the method. This catch prevents
// UnhandledPromiseRejectionWarnings from being thrown and prints the error to the
// console.
if (logActivityResult instanceof Promise) {
logActivityResult.catch(err => {
this.transcriptLoggerErrorHandler(err);
});
}
} catch (err) {
this.transcriptLoggerErrorHandler(err);
}
}
All in all, I don't think transcript logger middleware is the way to go here. While it may purport to serve your purposes, there are just too many problems with it. I would either write my own middleware or just put the middleware code directly in my bot logic like this:
async onTurn(turnContext) {
const activity = turnContext.activity;
await this.logActivity(turnContext, activity);
turnContext.onSendActivities(async (ctx, activities, next) => {
for (const activity of activities) {
await this.logActivity(ctx, activity);
}
return await next();
});
// Bot code here
// Save state changes
await this.userState.saveChanges(turnContext);
}
async logActivity(turnContext, activity) {
var transcript = await this.transcriptProperty.get(turnContext, []);
transcript.push(activity);
await this.transcriptProperty.set(turnContext, transcript);
console.log('Activities saved: ' + transcript.length);
}
Since your transcript would be stored in your user state, that user state would also have the account number you need and hopefully you'd be able to query for it.
Kyle's answer did help me solve the issue, and I think that will be the most reusable piece for anyone experiencing similar issues. The key takeaway is that, if you're using nodejs, you should not be using TranscriptLoggerMiddleware and instead use Kyle's function in your onTurn handler (repeated here for reference):
// Function provided by Kyle Delaney
async onTurn(turnContext) {
const activity = turnContext.activity;
await this.logActivity(turnContext, activity);
turnContext.onSendActivities(async (ctx, activities, next) => {
for (const activity of activities) {
await this.logActivity(ctx, activity);
}
return await next();
});
// Bot code here
// Save state changes
await this.userState.saveChanges(turnContext);
}
You need to note, though, that his logActivity function is just storing the raw activities to the user state using a custom transcriptProperty. As of yet I haven't found a good method to give business/admin users access to this data in a way that is easily readable and searchable, nor construct some sort of file out output to send to a customer requesting a transcript of their conversation. As such, I continued using my CustomLogger instead. Here is how I accomplished that.
First, you must create the transcriptLogger in the constructor. If you create it inside your turn handler, you will lose the cache/buffer and it will only have the latest activity instead of the full history. May be common sense but this tripped me up briefly. I do this in the constructor via this.transcriptLogger = new CustomerLogger(appInsightsClient);. I also modified my logActivity function to accept the userData (my state object) as a second, optional parameter. I have successfully been able to use that userData object to add the required customer information to the bot transcript. To modify Kyle's function above you just need to replace this.logActivity with your function call, in my case this.transcriptLogger.logActivity(context, userData);.
While there are still some other issues with this approach, it does solve the title question of how to get user state data into the transcript.
I'm developing an app that will allow members to book/pay for classes using Stripe. However, I'm finding it difficult to update the application to say that the user has paid successfully or not.
The aim is that when a user pays the payment will be assigned to them and they are able to get a list of payments within their account.
Below is the code i have within my controller:
[HttpPost]
public IActionResult PaymentSuccess(Event obj, int memberId, int eventId)
{
// add member to event attendees? if so just call BookEvent above
var eventBook = BookEvent(memberId, eventId);
var result = new { eventBook }; //service.UpdateEventAfterPayment(obj);
return Json(result);
}
public IActionResult Charge(string stripeEmail, string stripeToken, Event obj)
{
//var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
var customers = new Stripe.CustomerService();
var charges = new Stripe.ChargeService();
var customer = customers.Create(new Stripe.CustomerCreateOptions
{
Email = stripeEmail,
Source = stripeToken
}) ;
var charge = charges.Create(new Stripe.ChargeCreateOptions
{
Amount = 300,
Description = "Class Payment",
Currency="gbp",
Customer= customer.Id,
ReceiptEmail= stripeEmail,
Metadata = new Dictionary<string, string>()
{
{"OrderId", "123" },
{"PostCode", "BT480GY" }
}
});
if(charge.Status == "succeeded")
{
string BalanceTransactionId = charge.BalanceTransactionId;
var email = User.FindFirst("sub")?.Value;
var customerPaid = charge.Paid;
customerPaid = true;
return RedirectToAction(nameof(Index));
}
return RedirectToAction(nameof(Index));
}
}
And below is what is in my chtml file:
<div id="eventBook" class="text-center">
<form asp-action="Charge" asp-controller="Timetable" method="post">
<article>
<label>Amount: £3.00</label>
</article>
<script src="//checkout.stripe.com/v2/checkout.js"
class="stripe-button"
data-key="#Stripe.Value.PublishableKey"
data.locale="auto"
data-description="Class Charge">
</script>
</form>
</div>
I am not sure if I'm passing in the correct data from stripe to assign the payment to a specific user, that they can then go on and see a list of transactions.
As long as charges.Create() does not throw an exception you can assume that the charge succeeded. More on error handling here:
https://stripe.com/docs/api/errors/handling
After you create the customer, I'd recommend saving the Stripe customer ID to either your database or session. Later, when you want to list the charges made by that customer, you can make the "list charges" API call and pass that customer ID to get only charges for that customer:
https://stripe.com/docs/api/charges/list#list_charges-customer
var customerID = "cus_1234xxx..."; // Retrieved from session or DB
var options = new ChargeListOptions { Limit = 3, Customer = customerID };
var service = new ChargeService();
StripeList<Charge> charges = service.List(
options
);
Hope that clears things up!
Ideally, you should not be relying on a 1-1 correlation between a Stripe Customer and a user in your system. There's no need for a Stripe customer, unless you're persisting payment sources. Even if you are doing that, it shouldn't necessarily be a requirement that there's a Stripe Customer for every user.
The better approach here is to record the charge id on your end, and associate that with the user. Then, you should have a webhook endpoint for Stripe to notify when a charge goes through. When the webhook gets hit, then you can look up the payment/user on your side with that charge id associated with it, and update anything accordingly.
I've been trying for a project I'm working on to develop a function for a Food chatbot. What I'm currently working on is to perform a method for a user to make a purchase of an order that is stored in firebase realtime database.
The method is set as the method for an actionMap and the actionMap is linked to an intent for knowing when to call the method and for retrieving the parameters.
My current method uses a simple check for a user's existence and status within the database before identifying the existence of the order they're trying to make a purchase for by its id by going through the user's reference path and doing a .forEach to check every order found and look at its parent folder name to check if it matches the user's order id. My code is as follows:
const MakePurchaseACTION = 'Make Purchase';
function makePurchase(app){
let email = parameter.email;
let orderId = parameter.orderId;
var currDate = currDateGenerator();
var name = email.split(".com");
//Check if User exists first in database
var userRef = database.ref().child('user/' + name);
return userRef.once('value').then(function(snapshot) {
if (snapshot.exists()) {
let statusRetrieved = snapshot.child('Status').val();
//Check if user's status in database is signed in.
if (statusRetrieved == "Signed In") {
var orderRef = database.ref().child('order/' + name);
//Check the order table for the user.
return orderRef.once('value').then(function(orderSnapshot){
let orderVal = orderSnapshot.val();
console.log(orderVal);
//Check through every child for the matching id.
orderSnapshot.forEach(function(childSnapshot) {
let orderIdFound = childSnapshot.key;
//let cost = childSnapshot.child('Cost').val();
console.log(orderIdFound);
if(orderId == orderIdFound) {
let eateryName = childSnapshot.child('Eatery').val();
let eateryLocation = childSnapshot.child('EateryLocation').val();
let deliveryAddress = childSnapshot.child('DeliveryAddress').val();
let orderItem = childSnapshot.child('OrderItem').val();
let quantity = childSnapshot.child('Quantity').val();
let cost = childSnapshot.child('Cost').val();
var purchaseRef = database.ref().child('purchase/' + name + "/" + currDate + "/" + orderId);
purchaseRef.set({
"Eatery" : eateryName,
"EateryLocation" : eateryLocation,
"DeliveryAddress": deliveryAddress,
"OrderItem" : orderItem,
"Quantity": quantity,
"Cost": cost,
"DateCreated": currDate
});
app.add("You have successfully purchased Order " + orderId);
} else {
app.add("There is no order with that id.");
}
});
});
} else {
app.add("You need to be signed in before you can order!");
}
}
else {
app.add("Sorry pal you don't exist in the database.");
}
});
}
actionMap.set(MakePurchaseACTION, makePurchase);
After checking through some firebase logs
Firebase Logs screenshot here
Firebase Realtime Database Order Table Sample
I found that the method actually completes Purchase table sample but my dialogflow returns with the stated error of:
Error: No responses defined for platform: undefined and displays "Not Available" back to the user. My question is how do I go about resolving this error?
Building a Twitter bot to:
search for tweets with keywords like "Net Neutrality"
Return the tweet IDs and usernames for those tweets
Publish a tweet in response to that user (via in_reply_to_status_id, as described in Twitter Docs)
Here is my current code:
const Twitter = new twit(config);
let tweet = function() {
let params = {
q: '#netneutrality, #savethenet, Net Neutrality',
result_type: 'mixed',
lang: 'en'
}
// search through all tweets using our params and execute a function:
Twitter.get('search/tweets', params, function(err, data) {
// if there is no error
if (!err) {
// loop through the first 4 returned tweets
for (let i = 0; i < 4; i++) {
// iterate through those first four defining a rtId that is equal to the value of each of those tweets' ids
let rtId = data.statuses[i].id_str;
let username = data.statuses[i].username;
// the post action
Twitter.post('statuses/update', {
// setting the id equal to the rtId variable
in_reply_to_status_id: rtId
, status: `#${username} Send single-click #SaveTheNet tweets to key politicians at fliplist.app. Take 60 seconds to protect our Internet, once and for all.`
// log response and log error
}, function(err, response) {
if (response) {
console.log('Successfully tweeted');
}
if (err) {
console.log(err);
}
});
}
}
else {
// catch all log if the search could not be executed
console.log('Could not search tweets.');
}
});
}
tweet();
setInterval(tweet, 600000);
When I ran it, there was a successful output ("Succesfully tweeted" 4 times in terminal), and there were indeed 4 new tweets published from my account.
However, the username in all of those tweets was #undefined.
So I imagine I'm either failing to collect those usernames from the relevant tweets or failing to add them to the status string appropriately.
Any suggestions for how I can fix this?
For reference, here is a link to one of the #undefined tweets: https://twitter.com/ProoveTweets/status/1075855455958765569
And a screenshot is attached here
Two things to note. Firstly, Twitter's usernames are called "Screen Names". So you code should probably read:
let username = data.statuses[i].user.screen_name;
Take a look at the search documentation for an example of the JSON being returned. https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets.html
Secondly - please don't do this! Automatically replying to keywords is against the developer terms of service and is really annoying to users.
https://help.twitter.com/en/rules-and-policies/twitter-automation