I'm in the process of designing a chat bot and trying to find some Node.js sample code and/or documentation on how to implement the Azure Maps service as part of Bot Framework V4. There are many examples of how this is accomplished in V3, but there seems to be no examples of a V4 solution for Node.js. I'm looking to create a step in my botbuilder-dialog flow that would launch a simple "where do we ship it too" location dialog that would guide the user through the dialog and store the address results as part of that users profile. Any help or advice on this would be appreciated.
Yes, this is doable. I created a class (probably overkill, but oh well) in which I make my API call, with my supplied parameters, to get the map. I decided to use Azure Maps (vs Bing Maps) only because I was curious in how it differed. There isn't any reason you couldn't do this with Bing Maps, as well.
In the bot, I am using a component dialog because of how I have the rest of my bot designed. When the dialog ends, it will fall off the stack and return to the parent dialog.
In my scenario, the bot presents the user with a couple choices. "Send me a map" generates a map and sends it in an activity to the client/user. Anything else sends the user onward ending the dialog.
You will need to decide how you are getting the user's location. I developed this with Web Chat in mind, so I am getting the geolocation from the browser and returning it to the bot to be used when getMap() is called.
const { ActivityTypes, InputHints } = require('botbuilder');
const fetch = require('node-fetch');
class MapHelper {
async getMap(context, latitude, longitude) {
var requestOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow'
};
const result = await fetch(`https://atlas.microsoft.com/map/static/png?subscription-key=${ process.env.AZURE_MAPS_KEY }&api-version=1.0&layer=basic&zoom=13¢er=${ longitude },${ latitude }&language=en-US&pins=default|al.67|la12 3|lc000000||'You!'${ longitude } ${ latitude }&format=png`, requestOptions)
.then(response => response.arrayBuffer())
.then(async result => {
const bufferedData = Buffer.from(result, 'binary');
const base64 = bufferedData.toString('base64');
const reply = { type: ActivityTypes.Message };
const attachment = {
contentType: 'image/png',
contentUrl: `data:image/png;base64,${ base64 }`
};
reply.attachments = [attachment];
await context.sendActivity(reply, null, InputHints.IgnoringInput);
})
.catch(error => {
if (error) throw new Error(error);
});
return result;
};
};
module.exports.MapHelper = MapHelper;
const { ChoicePrompt, ChoiceFactory, ComponentDialog, ListStyle, WaterfallDialog } = require('botbuilder-dialogs');
const { MapHelper } = require('./mapHelper');
const CONFIRM_LOCALE_DIALOG = 'confirmLocaleDialog';
const CHOICE_PROMPT = 'confirmPrompt';
class ConfirmLocaleDialog extends ComponentDialog {
constructor() {
super(CONFIRM_LOCALE_DIALOG);
this.addDialog(new ChoicePrompt(CHOICE_PROMPT))
.addDialog(new WaterfallDialog(CONFIRM_LOCALE_DIALOG, [
this.askLocationStep.bind(this),
this.getMapStep.bind(this)
]));
this.initialDialogId = CONFIRM_LOCALE_DIALOG;
}
async askLocationStep(stepContext) {
const choices = ['Send me a map', "I'll have none of this nonsense!"];
return await stepContext.prompt(CHOICE_PROMPT, {
prompt: 'Good sir, may I pinpoint you on a map?',
choices: ChoiceFactory.toChoices(choices),
style: ListStyle.suggestedAction
});
}
async getMapStep(stepContext) {
const { context, context: { activity } } = stepContext;
const text = activity.text.toLowerCase();
if (text === 'send me a map') {
const { latitude, longitude } = activity.channelData;
const mapHelper = new MapHelper();
await mapHelper.getMap(context, latitude, longitude);
const message = 'Thanks for sharing!';
await stepContext.context.sendActivity(message);
return await stepContext.endDialog();
} else {
await stepContext.context.sendActivity('No map for you!');
return await stepContext.endDialog();
}
}
}
module.exports.ConfirmLocaleDialog = ConfirmLocaleDialog;
module.exports.CONFIRM_LOCALE_DIALOG = CONFIRM_LOCALE_DIALOG;
Hope of help!
---- EDIT ----
Per request, location data can be obtained from the browser using the below method. It is, of course, dependent on the user granting access to location data.
navigator.geolocation.getCurrentPosition( async (position) => {
const { latitude, longitude } = position.coords;
// Do something with the data;
console.log(latitude, longitude)
})
Related
While not a front-end developer, I'm trying to set up a web app to show up a demo for a product. That app is based on the Sigma.js demo app demo repository.
You'll notice that this app relies on a graph which is hosted locally, which is loaded as:
/src/views/Root.tsx :
useEffect(() => {
fetch(`${process.env.PUBLIC_URL}/dataset.json`)
.then((res) => res.json())
.then((dataset: Dataset) => {...
// do things ....
and I wish to replace this by a call to another service which I also host on Cloud Run.
My first guess was to use the gcloud-auth-library, but I could not make it work - especially since it does not seem to support Webpack > 5 (I might be wrong here), the point here this lib introduces many problems in the app, and I thought I'd be better off trying the other way GCP suggests to handle auth tokens: by calling the Metadata server.
So I replaced the code above with:
Root.tsx :
import { getData } from "../getGraphData";
useEffect(() => {
getData()
.then((res) => res.json())
.then((dataset: Dataset) => {
// do even more things!
getGraphData.js :
import { getToken } from "./tokens";
const graphProviderUrl = '<my graph provider service URL>';
export const getData = async () => {
try {
const token = await getToken();
console.log(
"getGraphData.js :: getData : received token",
token
);
const request = await fetch(
`${graphProviderUrl}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const data = await request.json();
console.log("getGraphData.js :: getData : received graph", data);
return data;
} catch (error) {
console.log("getGraphData.js :: getData : error getting graph data", error);
return error.message;
}
};
tokens.js :
const targetAudience = '<my graph provider service base URL>'; // base URL as audience
const metadataServerAddress = "169.254.169.254"; // use this to shortcut DNS call to metadata.google.internal
export const getToken = async () => {
if (tokenExpired()) {
const token = await getValidTokenFromServer();
sessionStorage.setItem("accessToken", token.accessToken);
sessionStorage.setItem("expirationDate", newExpirationDate());
return token.accessToken;
} else {
console.log("tokens.js 11 | token not expired");
return sessionStorage.getItem("accessToken");
}
};
const newExpirationDate = () => {
var expiration = new Date();
expiration.setHours(expiration.getHours() + 1);
return expiration;
};
const tokenExpired = () => {
const now = Date.now();
const expirationDate = sessionStorage.getItem("expirationDate");
const expDate = new Date(expirationDate);
if (now > expDate.getTime()) {
return true; // token expired
}
return false; // valid token
};
const getValidTokenFromServer = async () => {
// get new token from server
try {
const request = await fetch(`http://${metadataServerAddress}/computeMetadata/v1/instance/service-accounts/default/token?audience=${targetAudience}`, {
headers: {
'Metadata-Flavor': 'Google'
}
});
const token = await request.json();
return token;
} catch (error) {
throw new Error("Issue getting new token", error.message);
}
};
I know that this kind of call will need to be done server-side. What I don't know is how to have it happen on a React + Node app. I've tried my best to integrate good practices but most questions related to this topic (request credentials through a HTTP (not HTTPS!) API call) end with answers that just say "you need to do this server-side", without providing more insight into the implementation.
There is a question with similar formulation and setting here but the single answer, no upvote and comments is a bit underwhelming. If the actual answer to the question is "you cannot ever call the metadata server from a react app and need to set up a third-party service to do so (e.g. firebase)", I'd be keen on having it said explicitly!
Please assume I have only a very superficial understanding of node.js and React!
I am trying to develop a MS Teams bot that sends content to students module(unit) wise. I have created 3 classes:
methods.js = Contains all the methods for sending texts, attachments etc.
teamBot.js = Captures a specific keyword from the users and based on that executes a function.
test.js = Connects the bot with Airtable and sends the content accordingly
I am facing Cannot perform 'get' on a proxy that has been revoked error. I figured it might be because of the context. I am passing context as a parameter, which I feel might not be the correct way, how can I achieve the result, and retain the context between files.
teamsBot.js
const test = require("./test");
class TeamsBot extends TeamsActivityHandler {
constructor() {
super();
// record the likeCount
this.likeCountObj = { likeCount: 0 };
this.onMessage(async (context, next) => {
console.log("Running with Message Activity.");
let txt = context.activity.text;
// const removedMentionText = TurnContext.removeRecipientMention(context.activity);
// if (removedMentionText) {
// // Remove the line break
// txt = removedMentionText.toLowerCase().replace(/\n|\r/g, "").trim();
// }
// Trigger command by IM text
switch (txt) {
case "Begin": {
await test.sendModuleContent(context)
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
// Listen to MembersAdded event, view https://learn.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-notifications for more events
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let cnt = 0; cnt < membersAdded.length; cnt++) {
if (membersAdded[cnt].id) {
const card = cardTools.AdaptiveCards.declareWithoutData(rawWelcomeCard).render();
await context.sendActivity({ attachments: [CardFactory.adaptiveCard(card)] });
break;
}
}
await next();
});
}
test.js
const ms = require('./methods')
async function sendModuleContent(context) {
data = module_text //fetched from Airtable
await ms.sendText(context, data)
}
methods.js
const {TeamsActivityHandler, ActivityHandler, MessageFactory } = require('botbuilder');
async function sendText(context, text){
console.log("Sending text")
await context.sendActivity(text);
}
Refer this: TypeError: Cannot perform 'get' on a proxy that has been revoked
make the following changes to test.js
const {
TurnContext
} = require("botbuilder");
var conversationReferences = {};
var adapter;
async function sendModuleContent(context) {
data = module_text //fetched from Airtable
const currentUser = context.activity.from.id;
conversationReferences[currentUser] = TurnContext.getConversationReference(context.activity);
adapter = context.adapter;
await adapter.continueConversation(conversationReferences[currentUser], async turnContext => {
await turnContext.sendActivity(data);
});
}
Auth Dialog:
import { ChoicePrompt, DialogSet, DialogTurnStatus, OAuthPrompt, TextPrompt, WaterfallDialog, ComponentDialog } from 'botbuilder-dialogs';
import GraphClient from '../graph-client';
const MAIN_WATERFALL_DIALOG = 'mainWaterfallDialog';
const OAUTH_PROMPT = 'oAuthPrompt';
const CHOICE_PROMPT = 'choicePrompt';
const TEXT_PROMPT = 'textPrompt';
import moment = require('moment');
class AuthDialog extends ComponentDialog {
constructor() {
super('AuthDialog');
this.addDialog(new ChoicePrompt(CHOICE_PROMPT))
.addDialog(new OAuthPrompt(OAUTH_PROMPT, {
connectionName: process.env.ConnectionName,
text: 'Please login',
title: 'Login',
timeout: 300000
}))
.addDialog(new TextPrompt(TEXT_PROMPT))
.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.promptStep.bind(this),
this.processStep.bind(this)
]));
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
/**
* The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system.
* If no dialog is active, it will start the default dialog.
* #param {*} turnContext
* #param {*} accessor
*/
public async run(turnContext, accessor) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
public async promptStep(step) {
return step.beginDialog(OAUTH_PROMPT);
}
public async processStep(step) {
if (step.result) {
// We do not need to store the token in the bot. When we need the token we can
// send another prompt. If the token is valid the user will not need to log back in.
// The token will be available in the Result property of the task.
const tokenResponse = step.result;
// If we have the token use the user is authenticated so we may use it to make API calls.
if (tokenResponse && tokenResponse.token) {
await step.context.sendActivity(`Logged in.`);
} else {
await step.context.sendActivity('something wrong happened.');
}
} else {
await step.context.sendActivity('We couldn\'t log you in. Please try again later.');
}
return await step.endDialog();
}
}
export default AuthDialog;
I have a main dailog which is connected to luis and based on the intent recognized it executes corrosponding code:
for ex i have this in some cases:
case 'CalendarEvents':
return stepContext.beginDialog('AuthDialog');
const calendar = await new GraphClient('token').events();
let eventsBuilder: string = '';
// tslint:disable-next-line: prefer-for-of
for (let index = 0; index < calendar.length; index++) {
const element = calendar[index];
eventsBuilder += '\r\n' + moment(element.start.dateTime).format('dddd, MMMM Do YYYY, h:mm:ss a') + ' - ' + element.subject;
}
await step.context.sendActivity(`${eventsBuilder}`);
So if the intent is CalendarEvents then authenticate and than make some graph api call.
The problem I currently have is that the call to graph api is made before the auth is finished, I would like so first user authenticate and than receives some token and use that token for fetching graph api calls!
any idea how to achieve the above?
Please see the Graph Auth Sample. In particular,
It gets the token in MainDialog:
return step.beginDialog(OAUTH_PROMPT);
[...]
if (step.result) {
const tokenResponse = step.result;
if (tokenResponse && tokenResponse.token) {
const parts = (step.values.command || '').toLowerCase().split(' ');
const command = parts[0];
switch (command) {
case 'me':
await OAuthHelpers.listMe(step.context, tokenResponse);
break;
case 'send':
await OAuthHelpers.sendMail(step.context, tokenResponse, parts[1]);
break;
case 'recent':
await OAuthHelpers.listRecentMail(step.context, tokenResponse);
break;
default:
await step.context.sendActivity(`Your token is ${ tokenResponse.token }`);
}
}
}
[...]
Then, OAuthHelpers uses the token:
static async sendMail(context, tokenResponse, emailAddress) {
[...]
const client = new SimpleGraphClient(tokenResponse.token);
const me = await client.getMe();
await client.sendMail(
emailAddress,
'Message from a bot!',
`Hi there! I had this message sent from a bot. - Your friend, ${ me.displayName }`
);
await context.sendActivity(`I sent a message to ${ emailAddress } from your account.`);
}
This is how the sample works. For you, since you only want to do auth in the Auth dialog, you need to get the token from the user using the Auth dialog, then save it to their UserState, similar to this sample. You can then retrieve their UserState in any dialog and use the token if they have it.
Note
You can either use the Graph API through regular HTTP REST API requests, or use the Graph SDK like the sample does.
i am using nodejs v4 version of the botbuilder https://learn.microsoft.com/en-us/javascript/api/botbuilder/?view=botbuilder-ts-latest
My current code is picked from echo bot and looks like below
const { ActivityHandler } = require('botbuilder');
class ScanBuddyMsBot extends ActivityHandler {
constructor() {
super();
this.onMessage(async (context:any, next:any) => {
await context.sendActivity(`You said '${ context.activity.text }'`);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
}
module.exports.ScanBuddyMsBot = ScanBuddyMsBot;
I am looking a way to fetch user email sending message to my bot. I can see in the context activity, conversation id and service url but not the email id.
in another variation of this i am using below way to get email id and not sure how to make below code work for above
var bot = new builder.UniversalBot(connector, async function(session) {
var teamId = session.message.address.conversation.id;
connector.fetchMembers(
session.message.address.serviceUrl,
teamId,
async (err, result) => {
if (err) {
session.send('We faced an error trying to process this information', err);
return
}
else {
const email = result[0].email
}
In Bot Builder v4, you can access that REST API using the getConversationMembers function:
/**
*
* #param {TurnContext} turnContext
*/
async testTeams(turnContext) {
const activity = turnContext.activity;
const connector = turnContext.adapter.createConnectorClient(activity.serviceUrl);
const response = await connector.conversations.getConversationMembers(activity.conversation.id);
const email = response[0].email;
await turnContext.sendActivity(email);
}
Please refer to the documentation and the samples to better understand how to use the v4 SDK.
I have an application based on the React Starter Kit.
Every page have a fetch function that getting data from API in componentDidMount lifecycle.
I want to get data first and then render page with data and return it to the client. UX in my case no matter.
I know that RSK is isomorphic, I'm ready to change boilerplate or create my own. But I do not understand how to fetch data from API before render page(I mean how to tell express server what data requires).
How App fetching data now:
example_page.js:
import getBooks from 'queries/getAllBooks';
...
class IdTag extends React.Component {
componentDidMount(){
this.getBooks();
}
getBooks() => {
const request = getBooks();
request
.then(...)
}
}
getAllBooks.js:
import doGet from './doGet';
let result = '';
const request = async () => {
const reqUrl = '/api/books/';
result = await doGet(reqUrl);
return result;
};
export default request;
doGet.js:
const request = async reqUrl => {
let requestResult = null;
const doQuery = async () => {
const response = await fetch(reqUrl, {
method: 'GET',
});
const result = await response.json();
result.status = response.status;
return result;
};
requestResult = await doQuery();
return requestResult
}
...
export default request;
server.js:
...
app.get('/api/*', async (req, res) => {
const newUrl = config.gate.URL + req.url.replace('/api', '');
const accessToken = req.cookies.access_token;
const response = await nodeFetch(newUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const result = await response.json();
res.status(response.status);
res.json(result);
});
...
If each page has api calls, then Its better to use redux and redux saga. Purpose of redux saga is to handle api calls. It will process the actions in a Q. The moment u call api using fetch method, create below actioncreators
1) InitialLoading(true)
2) Fetch api call action creator
3) Based on success, error create action creator to store fetch method output data in store
4) InitialLoading(false)
You could simply set a flag when you begin your fetch, and while it's fetching return null instead of rendering. Something like:
flag = true;
request = getBooks();
request.then(flag = false);
and then:
render(){
if (flag){
return null;
} else {
return this.view;
}
}