i am using nodejs google cloud functions with ms bot framework. I have the invoke code looks like below:
const BotFrameworkAdapter = require('botbuilder').BotFrameworkAdapter
const { TeamsConversationBot } = require('./flashmsteamsbot');
const msadapter = new BotFrameworkAdapter({
appId: 'XXX',
appPassword: 'XXX'
});
const msteamsbot = new TeamsConversationBot()
const app = express();
app.post('/api/messages', (req:any, res:any) => {
msadapter.processActivity(req, res, async (context:any) => {
// Route to main dialog.
await msteamsbot.run(context)
});
});
the teams class looks like below:
const {
TurnContext,
TeamsActivityHandler,
CardFactory,
AttachmentLayoutTypes,
ActionTypes
} = require('botbuilder');
class TeamsConversationBot extends TeamsActivityHandler {
constructor() {
super();
this.onMessage(async (context:any, next:any) => {
TurnContext.removeRecipientMention(context.activity);
let msg = context.activity.text
const senderId = context.activity.from.aadObjectId
switch (msg) {
case 'don1':
await this.don1(context, keyword.trim(), userKey)
break;
default:
await this.help(context)
break;
}
await next();
});
this.onMembersAddedActivity(async (context:any, next:any) => {
functions.logger.log("start of onMembersAddedActivity", context)
context.activity.membersAdded.forEach(async (teamMember:any) => {
if (teamMember.id !== context.activity.recipient.id) {
await context.sendActivity(`Welcome to the team ${ teamMember.givenName } ${ teamMember.surname }`);
}
});
await next();
});
}
Whenever i send a message to the bot the this.onMessage is getting invoked. However, when i add a new member to a group where my bot is already present, the onMembersAddedActivity is not invoked. what i am missing here?
This is partially an issue in our docs and code comments, which I addressed here and here, respectively. The other issue is that you're using <method>Activity() instead of <method>Event().
The latest instructions are in the code comments, which just got merged, but basically,
Developers may handle Conversation Update activities sent from Microsoft Teams via two methods:
Overriding methods starting with on.. and not ending in ..Event() (e.g. onTeamsMembersAdded()), or instead
Passing callbacks to methods starting with on.. and ending in ...Event() (e.g. onTeamsMembersAddedEvent()),
to stay in line with older {#link ActivityHandler} implementation.
Developers should use either #1 or #2, above for all Conversation Update activities and not both #2 and #3 for the same activity. Meaning,
developers should override onTeamsMembersAdded() and not use both onTeamsMembersAdded() and onTeamsMembersAddedEvent().
Developers wanting to handle Invoke activities must override methods starting with handle...() (e.g. handleTeamsTaskModuleFetch()).
So, for you, you can either:
constructor() {
[...]
// This is passing in a callback
this.onTeamsMembersAddedEvent(async (
membersAdded: TeamsChannelAccount[],
teamInfo: TeamInfo,
context: TurnContext,
next: () => Promise<void>) => {
functions.logger.log("start of onMembersAddedActivity", context)
context.activity.membersAdded.forEach(async (teamMember:any) => {
if (teamMember.id !== context.activity.recipient.id) {
await context.sendActivity(`Welcome to the team ${ teamMember.givenName } ${ teamMember.surname }`);
}
});
await next();
});
}
or
constructor() {
[...]
}
[...]
// This is an override
async onTeamsMembersAdded(context: TurnContext): Promise<void> {
functions.logger.log("start of onMembersAddedActivity", context)
context.activity.membersAdded.forEach(async (teamMember:any) => {
if (teamMember.id !== context.activity.recipient.id) {
await context.sendActivity(`Welcome to the team ${ teamMember.givenName } ${ teamMember.surname }`);
}
});
}
Related
I'm trying to test a service that has a listener of the a custom Event Emitter in node with typescript and mocha, sinon.
My custom emmiter;
class PublishEmitter extends EventEmitter {
publish(id: string) {
this.emit('publish', id);
}
}
My service use case:
export default class PublishVehicle {
constructor(
private findVehicle: FindVehicle, // Service that contains find methods on repository
private updateVehicle: UpdateVehicle, // Service that contains update methods on repository
private logger: ILogger,
) {
this.producer = producer;
this.logger = logger;
}
listen() {
this.logger.log('debug', 'Creating listener on PublishEmitter');
this.publishListener = this.publishListener.bind(this);
pubsub.on('publish', this.publishListener);
}
/**
* Listener on PublishEmitter.
*
* #param event
*/
async publishListener(event: string) {
try {
const vehicle = await this.findVehicle.findById(event);
if (vehicle?.state === State.PENDING_PUBLISH) {
//
const input = { state: State.PUBLISH };
await this.updateVehicle.update(vehicle.id, input);
this.logger.log('debug', `Message sent at ${Date.now() - now} ms`);
}
this.logger.log('debug', `End Vehicle's Publish Event: ${event}`);
} catch (error) {
this.logger.log('error', {
message: `publishListener: ${event}`,
stackTrace: error,
});
}
}
}
and in my test file:
import chai from 'chai';
const { expect } = chai;
import sinon from 'sinon';
import { StubbedInstance, stubInterface } from 'ts-sinon';
import pubsub from './PublishEmitter';
describe('Use Case - Publish Vehicle', function () {
let mockRepository: MockVehicleRepository;
let publishVehicle: PublishVehicle;
let findVehicleUseCase: FindVehicle;
let updateVehicleUseCase: UpdateVehicle;
before(() => {
const logger = Logger.getInstance();
mockRepository = new MockVehicleRepository();
findVehicleUseCase = new FindVehicle(mockRepository, logger);
updateVehicleUseCase = new UpdateVehicle(mockRepository);
publishVehicle = new PublishVehicle(
findVehicleUseCase,
updateVehicleUseCase,
logger,
);
});
afterEach(() => {
// Restore the default sandbox here
sinon.restore();
});
it('Should emit event to publish vehicle', async () => {
const vehicle = { ... }; // dummy data
const stubFindById = sinon
.stub(mockRepository, 'findById')
.returns(Promise.resolve(vehicle));
const stubUpdate = sinon
.stub(mockRepository, 'update')
.returns(Promise.resolve(vehicle));
const spy = sinon.spy(publishVehicle, 'publishListener');
publishVehicle.listen();
pubsub.publish(vehicle.id);
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // Error (0 call)
expect(stubUpdate.calledOnce).to.be.true; // Error (0 call)
});
});
When I debug this test, indeed the methods are called but they seem to be executed after it has gone through the last expect lines.
The output:
1 failing
1) Use Case - Publish Vehicle
Should emit event to publish vehicle:
AssertionError: expected false to be true
+ expected - actual
-false
+true
UPDATE
Finally I was be able to solve my problem wrapping expect lines in setTimeout.
setTimeout(() => {
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // OK
expect(stubUpdate.calledOnce).to.be.true; // OK
done();
}, 0);
I'm create a hook file with the following information, which is Hooks.js
Hooks.js is working to authenticate an actions with JWT when need it, I dont need it in all servies calls.
As my understanding the syntax to call a hook was app/use route/hooks and those hooks were only applied to and specific route and not globally.
module.exports = {
errorHandler: (context) => {
if (context.error) {
context.error.stack = null;
return context;
}
},
isValidToken: (context) => {
const token = context.params.headers.authorization;
const payload = Auth.validateToken(token);
console.log(payload);
if(payload !== "Invalid" && payload !== "No Token Provided"){
context.data = payload._id;
}
else {
throw new errors.NotAuthenticated('Authentication Error Token');
}
},
isValidDomain: (context) => {
if (
config.DOMAINS_WHITE_LIST.includes(
context.params.headers.origin || context.params.headers.host
)
) {
return context;
}
throw new errors.NotAuthenticated("Not Authenticated Domain");
},
normalizedId: (context) => {
context.id = context.id || context.params.route.id;
},
normalizedCode: (context) => {
context.id = context.params.route.code;
},
};
Then I create a file for services and routes, like the following:
const Hooks = require("../../Hooks/Hooks");
const userServices = require("./user.services");
module.exports = (app) => {
app
.use("/users", {
find: userServices.find,
create: userServices.createUser,
})
.hooks({
before: {
find: [Hooks.isValidDomain],
create: [Hooks.isValidDomain],
},
});
app
.use("/users/:code/validate", {
update: userServices.validateCode,
})
.hooks({
before: {
update: [Hooks.isValidDomain, Hooks.normalizedCode],
},
});
app
.use("/users/personal", {
update: userServices.personalInfo,
})
.hooks({
before: {
update: [Hooks.isValidDomain, Hooks.isValidToken],
},
});
};
Why Hooks.isValidToken applies to all my update methods? Even if I'm not calling it?
Please help.
app.hooks registers an application level hook which runs for all services. If you only want it for a specific service and method it needs to be app.service('users').hooks().
I'm currently building a provisioning bot using v4 of the Bot Framework and I've integrated it with the Microsoft Graph.
The Microsoft Graph is being used to validate user inputs, so in this scenario, it's checking to see if the group name already exists. However, the issue I'm running into is getting the bot to repeat the previous step if the validation finds the group exists.
I've read through the forum and seen a number of solutions, particularly, I have come across the step.activeDialog.state['stepIndex']-2 approach, but have been unable to get it to work. Is this a viable solution for going back a step in NodeJS or should I be looking at another approach?
async nameStep(step) {
// User selected a group type and now is required to enter the name of the group
step.values.sitetype = step.result.value;
return await step.prompt(NAME_PROMPT, 'What do you want to name it');
}
async ownerStep(step) {
// Retrieve the value from the previous step and check against the Microsoft Graph to see if the name has been used previously
step.values.name = step.result;
const getToken =
await axios.post(TOKEN_ENDPOINT, qs.stringify(postData))
.then(response => {
return {
headers: {
'Authorization': 'Bearer ' + response.data.access_token
}
}
})
.catch(error => {
console.log(error);
});
const graphCall =
await axios.get("https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'" + `${step.result}` + "')", getToken)
.then((response) => {
if (response.data.value[0] != null) {
return true;
}
})
.catch((error) => {
console.log(error);
})
if (!graphCall) {
return await step.prompt(NAME_PROMPT, 'What is your email address');
} else {
await step.context.sendActivity("Group already exists");
return await step.activeDialog.state['stepIndex']-2
}
}
Thanking you in advance
You can achieve this by use of a component dialog. Essentially, you extrapolate the steps you would like to repeat into a separate dialog that is called only from within the current (parent) dialog. In the parent, you institute your checks. When a check fails, the component dialog is called again. If it succeeds, the parent dialog continues on.
In the code below, my parent dialog immediately calls the component dialog for a first pass thru presenting the user with two options. Each will send a pre-determined text value which is checked to see if a LUIS intent exists for it.
The first option, "Hello", will succeed with an intent having been found. It then restarts the parent dialog. The parent dialog starts with the text "You have a choice to make in life..." which will re-display as the parent dialog begins again.
The second option will fail and returns the user to the component dialog to try again. The component dialog starts with "Text me something! I'll see if my maker setup a LUIS intent for it." This text will display when either button is clicked because the component dialog is run in both instances. However, only this text will display when LUIS fails to find an intent and restarts the component dialog.
Side note - the parent dialog in this example is, in fact, a component dialog to my main dialog which is why it is exported at the end. So, yes, you can have component dialogs within component dialogs.
Parent Dialog:
const { ComponentDialog, WaterfallDialog } = require('botbuilder-dialogs');
const { LuisRecognizer } = require('botbuilder-ai');
const { ChoiceDialogSub, CHOICE_DIALOG_SUB } = require('./choiceDialog_Sub');
const CHOICE_DIALOG = 'choiceDialog';
class ChoiceDialog extends ComponentDialog {
constructor(id) {
super(id);
this.addDialog(new ChoiceDialogSub(CHOICE_DIALOG_SUB));
this.addDialog(new WaterfallDialog(CHOICE_DIALOG, [
this.welcomeStep.bind(this),
this.choiceLuisStep.bind(this)
]));
this.initialDialogId = CHOICE_DIALOG;
try {
this.recognizer = new LuisRecognizer({
applicationId: process.env.LuisAppId,
endpointKey: process.env.LuisAPIKey,
endpoint: `https://${ process.env.LuisAPIHostName }`
}, {}, true);
} catch (err) {
console.warn(`LUIS Exception: ${ err } Check your LUIS configuration`);
}
}
async welcomeStep(stepContext) {
await stepContext.context.sendActivity('You have a choice to make in life...');
return await stepContext.beginDialog(CHOICE_DIALOG_SUB);
}
async choiceLuisStep(stepContext) {
if (stepContext.context.activity.text) {
const stepResults = stepContext.context.activity.text;
const recognizerResult = await this.recognizer.recognize(stepContext.context);
const intent = await LuisRecognizer.topIntent(recognizerResult);
if (intent === 'Greeting') {
await stepContext.context.sendActivity(`'${ stepResults }' identified in the {${ intent }} intent.`);
return await stepContext.beginDialog(CHOICE_DIALOG);
} else {
await stepContext.context.sendActivity(`No LUIS intent was found for '${ stepResults }'.`);
return await stepContext.beginDialog(CHOICE_DIALOG_SUB);
}
} else {
await stepContext.context.sendActivity('I need text, fool!');
return await stepContext.next();
}
}
}
module.exports.ChoiceDialog = ChoiceDialog;
module.exports.CHOICE_DIALOG = CHOICE_DIALOG;
Component Dialog:
const { ChoicePrompt, ChoiceFactory, ComponentDialog, WaterfallDialog } = require('botbuilder-dialogs');
const CHOICE_DIALOG_SUB = 'choiceDialogSub';
const CHOICE_DIALOG_SUB_PROMPT = 'choicePromptSub';
class ChoiceDialogSub extends ComponentDialog {
constructor(id) {
super(id);
this.addDialog(new ChoicePrompt(CHOICE_DIALOG_SUB_PROMPT))
.addDialog(new WaterfallDialog(CHOICE_DIALOG_SUB, [
this.choiceStep.bind(this)
]));
this.initialDialogId = CHOICE_DIALOG_SUB;
}
async choiceStep(stepContext) {
const choices = ['Hello', 'No soup for you!'];
return await stepContext.prompt(CHOICE_DIALOG_SUB_PROMPT, {
prompt: "Text me something! I'll see if my maker setup a LUIS intent for it.",
choices: ChoiceFactory.toChoices(choices)
});
}
}
module.exports.ChoiceDialogSub = ChoiceDialogSub;
module.exports.CHOICE_DIALOG_SUB = CHOICE_DIALOG_SUB;
Hope of help!
I have created a GitHub probot app using nodejs and typescript. I am listening on pull_request event. How do I retrieve pr_number from the probot context object?
following is the code in intex.ts
export = (app: Application) => {
app.on('pull_request', async (context) => {
})
}
The field that you're interested in is context.payload inside the callback:
export = (app: Application) => {
app.on('pull_request', async (context) => {
const payload = context.payload
// ...
})
}
This matches the payloads listed in the GitHub Webhook Events page: https://developer.github.com/webhooks/#events
You're interested in the pull_request payload which can be found here: https://developer.github.com/v3/activity/events/types/#pullrequestevent
And pull_request.number is your relevant piece of information you need:
export = (app: Application) => {
app.on('pull_request', async (context) => {
const payload = context.payload
const number = payload.pull_request.number
})
}
in my Node.JS project (a backend for an Angular 5 project) I have created a service that deals with the AWS Authentication... I have called this awsAuthenticationService. All works well but I now need to test it. In my awsAuthenticationService.js I have the following method that has some minor logic and then calls a method provided by the "cognitoIdentityServiceProvider". Here is a snippet of my code (I really have reduced this)
constructor() {
this._cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider(this.cognitoConfig);
}
toggleUserAccess(userName, type) {
const params = {
Username: userName,
UserPoolId: this.cognitoConfig.userPoolId
};
if (type === null) {
return this._cognitoIdentityServiceProvider.adminEnableUser(params).promise();
}
return this._cognitoIdentityServiceProvider.adminDisableUser(params).promise();
}
As you can see from the toggleUserAccess we pass a few parameters, determine what they are then call the appropriate method. I wish to test this by having a unit test that will call the authenticationService.toggleUserAccess, pass some params and spy on the authenticationService._cognitoIdentityServiceProvider methods to see if they were called. I set it up so...
let authenticationService = require('./awsAuthenticationService');
describe('toggleUserAccess', () => {
beforeEach(() => {
authenticationService._cognitoIdentityServiceProvider = {
adminDisableUser(params) {
return {
promise() {
return Promise.resolve(params);
}
};
}
};
authenticationService._cognitoIdentityServiceProvider = {
adminEnableUser(params) {
return {
promise() {
return Promise.resolve(params);
}
};
}
};
});
it('should call adminEnableUser if the type is null', () => {
authenticationService.toggleUserAccess('TheUser', null);
const spyCognito = sinon.spy(authenticationService._cognitoIdentityServiceProvider, 'adminEnableUser');
expect(spyCognito.calledOnce).to.equal(true);
});
it('should call adminDisableUser if the type is null', () => {
authenticationService.toggleUserAccess('TheUser', '0001');
const spyCognito = sinon.spy(authenticationService._cognitoIdentityServiceProvider, 'adminDisableUser');
expect(spyCognito.calledOnce).to.equal(true);
});
});
My tests aren't passing and I think I have set up my sinon.spys incorrectly - can anyone see what I am doing wrong or give advice please
To stub class of AWS.CognitoIdentityServiceProvider, need to stub with its prototype keyword.
// add require statement for your AWS class
const spyCognito = sinon.spy(AWS.CognitoIdentityServiceProvider.prototype, 'adminDisableUser');
expect(spyCognito.calledOnce).to.equal(true);
Hope it helps