This commit is contained in:
Nilesh Khaitan 2019-01-10 12:35:00 -08:00 коммит произвёл GitHub
Родитель 744e878b5b
Коммит b967084d63
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 1431 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,9 @@
{
"name": "{WEB_SITE_NAME}",
"description": "{WEB_SITE_NAME} Azure Bot Service Code",
"homepage": "https://github.com",
"private": false,
"has_issues": true,
"has_projects": true,
"has_wiki": true
}

Просмотреть файл

@ -0,0 +1,58 @@
rem @echo off
setlocal
SET password=%1
SET repoName=srcRepo
SET repoUrl=file:///%HOMEDRIVE:~0,1%/%HOMEPATH:~1%/site/%repoName%
SET download=bot-src
echo %repoUrl%
rem cd to project root
pushd ..\wwwroot
rem init git
call git init
call git config user.name "botframework"
call git config user.email "util@botframework.com"
call git add .
call git commit -m "prepare to download source"
call git remote add srcRepo %repoUrl%
popd
rem init upstream
pushd %HOME%\site
mkdir srcRepo
cd srcRepo
call git init --bare
popd
rem push to upstream
pushd ..\wwwroot
call git push --set-upstream srcRepo master
popd
rem clone srcRepo
pushd %HOME%\site
call git clone %repoUrl% %download%
rem delete .git
cd %download%
call rm -r -f .git
popd
rem prepare for publish
type PostDeployScripts\publish.js.template | sed -e s/\{WEB_SITE_NAME\}/%WEBSITE_SITE_NAME%/g | sed -e s/\{PASSWORD\}/%password%/g > %HOME%\site\%download%\publish.js
rem preare the zip file
%HOMEDRIVE%\7zip\7za a %HOME%\site\%download%.zip %HOME%\site\%download%\*
rem cleanup git stuff
pushd ..\wwwroot
call rm -r -f .git
popd
pushd %HOME%\site
call rm -r -f %download%
call rm -r -f %repoName%
popd
endlocal

Просмотреть файл

@ -0,0 +1,52 @@
var zipFolder = require('zip-folder');
var path = require('path');
var fs = require('fs');
var request = require('request');
var rootFolder = path.resolve('.');
var zipPath = path.resolve(rootFolder, '../{WEB_SITE_NAME}.zip');
var kuduApi = 'https://{WEB_SITE_NAME}.scm.azurewebsites.net/api/zip/site/wwwroot';
var userName = '${WEB_SITE_NAME}';
var password = '{PASSWORD}';
function uploadZip(callback) {
fs.createReadStream(zipPath).pipe(request.put(kuduApi, {
auth: {
username: userName,
password: password,
sendImmediately: true
},
headers: {
"Content-Type": "applicaton/zip"
}
}))
.on('response', function(resp){
if (resp.statusCode >= 200 && resp.statusCode < 300) {
fs.unlink(zipPath);
callback(null);
} else if (resp.statusCode >= 400) {
callback(resp);
}
})
.on('error', function(err) {
callback(err)
});
}
function publish(callback) {
zipFolder(rootFolder, zipPath, function(err) {
if (!err) {
uploadZip(callback);
} else {
callback(err);
}
})
}
publish(function(err) {
if (!err) {
console.log('{WEB_SITE_NAME} publish');
} else {
console.error('failed to publish {WEB_SITE_NAME}', err);
}
});

Просмотреть файл

@ -0,0 +1,10 @@
@echo off
setlocal
echo record deployment timestamp
date /t >> ..\deployment.log
time /t >> ..\deployment.log
echo ---------------------- >> ..\deployment.log
echo Deployment done

Просмотреть файл

@ -0,0 +1,44 @@
@echo off
setlocal
rem ------------------------------------------------------------------------------------------
rem setupVsoRemoteRepo [remoteUser] [personalAccessToken] [projName{optional}]
rem create and populate VSO git repo for the ABS code instance
rem
rem remoteUser: user account name of the personal access token
rem personalAccessToken: the personal access token used to access github REST API (requires repos scope)
rem projName the name of the project to create (default to WEBSITE_SITE_NAME)
rem ------------------------------------------------------------------------------------------
set remoteUrl=https://api.github.com
set remoteUser=%1
set remotePwd=%2
set projName=%3
if '%projName%'=='' set projName=%WEBSITE_SITE_NAME%
set repoUrl=https://%remoteUser%:%remotePwd%@github.com/%remoteUser%/%projName%.git
rem use curl to create project
pushd ..\wwwroot
type PostDeployScripts\githubProject.json.template | sed -e s/\{WEB_SITE_NAME\}/%projName%/g > %TEMP%\githubProject.json
call curl -H "Content-Type: application/json" -u %remoteUser%:%remotePwd% -d "@%TEMP%\githubProject.json" -X POST %remoteUrl%/user/repos
rem rm %TEMP%\githubProject.json
popd
popd
rem cd to project root
pushd ..\wwwroot
rem init git
call git init
call git config user.name "%remoteUser%"
call git config user.password "%remotePwd%"
call git config user.email "util@botframework.com"
call git add .
call git commit -m "prepare to setup source control"
call git push %repoUrl% master
popd
rem cleanup git stuff
pushd ..\wwwroot
call rm -r -f .git
popd
endlocal

Просмотреть файл

@ -0,0 +1,50 @@
@echo off
setlocal
rem ------------------------------------------------------------------------------------------
rem setupVsoRemoteRepo [vsoRemote] [vsoUserName] [vsoPersonalAccessToken] [projName{optional}]
rem create and populate VSO git repo for the ABS code instance
rem
rem vsoRmote: url of the VSO site (e.g. https://awesomebot.visualstudio.com )
rem vosUserName: user account name of the personal access token
rem vsoPersonalAccessToken: the personal access token used to access VSO REST api
rem projName the name of the project to create (default to WEBSITE_SITE_NAME)
rem ------------------------------------------------------------------------------------------
set remoteUrl=%1
set remoteUser=%2
set remotePwd=%3
set projName=%4
if '%projName%'=='' set projName=%WEBSITE_SITE_NAME%
set vstsRoot=%remoteUrl%
set repoUrl=https://%remoteUser%:%remotePwd%@%remoteUrl:~8%/_git/%projName%
set vstsCreateProject=https://%remoteUser%:%remotePwd%@%remoteUrl:~8%/defaultcollection/_apis/projects?api-version=3.0
rem use curl to create project
pushd ..\wwwroot
type PostDeployScripts\vsoProject.json.template | sed -e s/\{WEB_SITE_NAME\}/%projName%/g > %TEMP%\vsoProject.json
call curl -H "Content-Type: application/json" -d "@%TEMP%\vsoProject.json" -X POST %vstsCreateProject%
rm %TEMP%\vsoProject.json
rem sleep for 15 seconds for the creation to complete, this is a wild guess
call sleep 15
popd
popd
rem cd to project root
pushd ..\wwwroot
rem init git
call git init
call git config user.name "%remoteUser%"
call git config user.password "%remotePwd%"
call git config user.email "util@botframework.com"
call git add .
call git commit -m "prepare to setup source control"
call git push %repoUrl% master
popd
rem cleanup git stuff
pushd ..\wwwroot
call rm -r -f .git
popd
endlocal

Просмотреть файл

@ -0,0 +1,12 @@
{
"name": "{WEB_SITE_NAME}",
"description": "{WEB_SITE_NAME} Azure Bot Service Code",
"capabilities": {
"versioncontrol": {
"sourceControlType": "Git"
},
"processTemplate": {
"templateTypeId": "6b724908-ef14-45cf-84f8-768b5384da45"
}
}
}

57
README.MD Normal file
Просмотреть файл

@ -0,0 +1,57 @@
# Echo Bot template
This sample shows how to create a simple echo bot with state. The bot maintains a simple counter that increases with each message from the user. This bot example uses [`restify`](https://www.npmjs.com/package/restify).
# Prerequisite to run this bot locally
- Download the bot code from the Build blade in the Azure Portal
- Create a file called .env in the root of the project and add the botFilePath and botFileSecret to it
- You can find the botFilePath and botFileSecret in the Azure App Service application settings
- Your .env file should look like this
```bash
botFilePath=<copy value from App settings>
botFileSecret=<copy value from App settings>
```
- Run `npm install` in the root of the bot project
- Finally run `npm start`
## Testing the bot using Bot Framework Emulator
[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
- Install the Bot Framework Emulator from [here](https://aka.ms/botframework-emulator)
### Connect to bot using Bot Framework Emulator v4
- Launch the Bot Framework Emulator
- File -> Open bot and navigate to the bot project folder
- Select `<your-bot-name>.bot` file
# Bot state
A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of conversation state or store user related information for longer than the lifetime of one given conversation.
In this example, the bot's state is used to track number of messages.
A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides classes for [storing and retrieving state data](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&tabs=js) as an object associated with a user or a conversation.
- Conversation properties help your bot keep track of the current conversation the bot is having with the user. If your bot needs to complete a sequence of steps or switch between conversation topics, you can use conversation properties to manage steps in a sequence or track the current topic. Since conversation properties reflect the state of the current conversation, you typically clear them at the end of a session, when the bot receives an end of conversation activity.
- User properties can be used for many purposes, such as determining where the user's prior conversation left off or simply greeting a returning user by name. If you store a user's preferences, you can use that information to customize the conversation the next time you chat. For example, you might alert the user to a news article about a topic that interests her, or alert a user when an appointment becomes available. You should clear them if the bot receives a delete user data activity.
# Deploy this bot to Azure
You can use the [MSBot](https://github.com/microsoft/botbuilder-tools) Bot Builder CLI tool to clone and configure any services this sample depends on.
To install all Bot Builder tools -
```bash
npm i -g msbot chatdown ludown qnamaker luis-apis botdispatch luisgen
```
To clone this bot, run
```
msbot clone services -f deploymentScripts/msbotClone -n <BOT-NAME> -l <Azure-location> --subscriptionId <Azure-subscription-id>
```
# Further reading
- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0)
- [Write directly to storage](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=jsechoproperty%2Ccsetagoverwrite%2Ccsetag)
- [Managing conversation and user state](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&tabs=js)

468
core/bot.js Normal file
Просмотреть файл

@ -0,0 +1,468 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ActionTypes, ActivityTypes, CardFactory } = require('botbuilder');
const { DialogContext, DialogSet, WaterfallDialog, WaterfallStepContext } = require('botbuilder-dialogs');
const { Login, LOGIN_PROMPT } = require('./login');
const { Moodle } = require('./moodle');
const { LuisRecognizer } = require('botbuilder-ai');
const OAUTH_CONNECTION = process.env.oAuthConnection || false;
if(!OAUTH_CONNECTION) {
console.error("Please set up environment variable 'oAuthConnection'");
process.exit(1);
}
// Used to create the BotStatePropertyAccessor for storing the user's language preference.
const LANGUAGE_PREFERENCE = 'language_preference';
var DEFAULT_AVAILABLE_LANGUAGES = process.env.availableLanguages || 'en,es';
DEFAULT_AVAILABLE_LANGUAGES = DEFAULT_AVAILABLE_LANGUAGES.split(',').map(function(item) {
return item.toLowerCase().trim();
});
//Default bot language
const DEFAULT_LANGUAGE = process.env.defaultLanguage || 'en';
//Setting up LUIS instance for each available language
const LUIS_INSTANCES = {};
for (let lang of DEFAULT_AVAILABLE_LANGUAGES){
LUIS_INSTANCES[lang] = {
APPLICATION_ID : process.env["luisApplicationId_"+lang]|| false,
ENDPOINT : process.env["luisEndpoint_"+lang] || false,
ENDPOINT_KEY : process.env["luisEndpointKey_"+lang] || false
}
}
if(!LUIS_INSTANCES[DEFAULT_LANGUAGE].APPLICATION_ID) {
console.error(`Please set up environment variable 'luisApplicationId_${ DEFAULT_LANGUAGE }'`);
process.exit(1);
}
if(!LUIS_INSTANCES[DEFAULT_LANGUAGE].ENDPOINT) {
console.error(`Please set up environment variable 'luisEndpoint_${ DEFAULT_LANGUAGE }'`);
process.exit(1);
}
if(!LUIS_INSTANCES[DEFAULT_LANGUAGE].ENDPOINT_KEY) {
console.error(`Please set up environment variable 'luisEndpointKey_${ DEFAULT_LANGUAGE }'`);
process.exit(1);
}
class MoodleWsBot {
/*
* @param {ConversationState} conversationState The state that will contain the DialogState BotStatePropertyAccessor.
*/
constructor(conversationState, userState, translator) {
this.conversationState = conversationState;
this.userState = userState;
this.translator = translator;
// Create property for languge selection
this.languagePreferenceProperty = this.userState.createProperty(LANGUAGE_PREFERENCE);
// Add the LUIS recognizer for each available language.
this.luisRecognizer = {};
for (let lang of DEFAULT_AVAILABLE_LANGUAGES){
if(LUIS_INSTANCES[lang].APPLICATION_ID){
this.luisRecognizer[lang] = new LuisRecognizer({
applicationId: LUIS_INSTANCES[lang].APPLICATION_ID,
endpoint: LUIS_INSTANCES[lang].ENDPOINT,
endpointKey: LUIS_INSTANCES[lang].ENDPOINT_KEY
});
}
}
// DialogState property accessor. Used to keep persist DialogState when using DialogSet.
this.dialogState = conversationState.createProperty('dialogState');
this.commandState = conversationState.createProperty('commandState');
// Create a DialogSet that contains the OAuthPrompt.
this.dialogs = new DialogSet(this.dialogState);
// Add an OAuthPrompt with the connection name as specified on the Bot's settings blade in Azure.
this.dialogs.add(Login.prompt(OAUTH_CONNECTION, this.translator[DEFAULT_LANGUAGE]));
this._graphDialogId = 'graphDialog';
// Logs in the user and calls proceeding dialogs, if login is successful.
this.dialogs.add(new WaterfallDialog(this._graphDialogId, [
this.promptStep.bind(this),
this.processStep.bind(this)
]));
};
/**
* This controls what happens when an activity get sent to the bot.
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
*/
async onTurn(turnContext) {
const dc = await this.dialogs.createContext(turnContext);
switch (turnContext.activity.type) {
case ActivityTypes.Message:
await this.processInput(dc);
break;
case ActivityTypes.Event:
case ActivityTypes.Invoke:
// Sanity check the Activity type and channel Id.
if (turnContext.activity.type === ActivityTypes.Invoke && turnContext.activity.channelId !== 'msteams') {
throw new Error('The Invoke type is only valid on the MS Teams channel.');
};
await dc.continueDialog();
if (!turnContext.responded) {
await dc.beginDialog(this._graphDialogId);
};
break;
case ActivityTypes.ConversationUpdate:
await this.sendWelcomeMessage(turnContext);
break;
default:
await turnContext.sendActivity(`[${ turnContext.activity.type }]-type activity detected.`);
}
await this.conversationState.saveChanges(turnContext);
await this.userState.saveChanges(turnContext);
};
/**
* Creates a Hero Card that is sent as a welcome message to the user.
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
*/
async sendWelcomeMessage(turnContext) {
const userLanguage = await this.languagePreferenceProperty.get(turnContext, DEFAULT_LANGUAGE);
const activity = turnContext.activity;
if (activity && activity.membersAdded) {
const heroCard = CardFactory.heroCard(
this.translator[userLanguage].__('Hello!'),
undefined,
CardFactory.actions([
{
type: ActionTypes.ImBack,
title: this.translator[userLanguage].__('Help'),
value: 'help'
}
]),
{text: this.translator[userLanguage].__("I am Moodle Assistant, a bot that answers questions about your assignments and courses. <br/><br/> If you are curious about what I can do, just type 'help' or click on the button below and I will give you the list of questions I can answer!")}
);
for (const idx in activity.membersAdded) {
if (activity.membersAdded[idx].id !== activity.recipient.id) {
await turnContext.sendActivity({ attachments: [heroCard] });
}
}
}
}
/**
* Creates a Thumbnail Card that is sent as a feedback message to the user.
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
*/
async sendFeedbackMessage(turnContext) {
const userLanguage = await this.languagePreferenceProperty.get(turnContext, DEFAULT_LANGUAGE);
const feedbackCard = CardFactory.thumbnailCard('', undefined, CardFactory.actions([
{
type: 'openUrl',
title: this.translator[userLanguage].__('Give feedback'),
value: 'https://microsoftteams.uservoice.com/forums/916759-moodle'
}
]), {text: this.translator[userLanguage].__('Please give us feedback by clicking on the button below.')}
);
await turnContext.sendActivity({ attachments: [feedbackCard] });
}
/**
* Checks and changes User State language if needed
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
*/
async changeLanguage(turnContext, language, userLanguage) {
if (isLanguageChangeRequested(language, userLanguage)) {
await this.languagePreferenceProperty.set(turnContext, language);
await this.userState.saveChanges(turnContext);
return true;
}else{
return false;
}
}
/**
* Processes input and route to the appropriate step.
* @param {DialogContext} dc DialogContext
*/
async processInput(dc) {
const userLanguage = await this.languagePreferenceProperty.get(dc.context, DEFAULT_LANGUAGE);
if(dc.context.activity.channelData.team != undefined){
await dc.context.sendActivity(this.translator[userLanguage].__('The answer to your query can not be displayed in team conversation. Please ask me the same question in personal chat.'));
}else{
switch (dc.context.activity.text.toLowerCase()) {
case 'signoff':
case 'logoff':
case 'signout':
case 'logout':
const botAdapter = dc.context.adapter;
await botAdapter.signOutUser(dc.context, OAUTH_CONNECTION);
await dc.context.sendActivity(this.translator[userLanguage].__('You are now signed out.'));
break;
default:
// The waterfall dialog to handle the input.
await dc.continueDialog();
if (!dc.context.responded) {
await dc.beginDialog(this._graphDialogId);
}
}
}
};
/**
* WaterfallDialogStep for storing commands and beginning the OAuthPrompt.
* Saves the user's message as the command to execute if the message is not
* a magic code.
* @param {WaterfallStepContext} step WaterfallStepContext
*/
async promptStep(step) {
const activity = step.context.activity;
if (activity.type === ActivityTypes.Message && !(/\d{6}/).test(activity.text)) {
await this.commandState.set(step.context, activity.text);
await this.conversationState.saveChanges(step.context);
}
return await step.beginDialog(LOGIN_PROMPT);
}
/**
* WaterfallDialogStep to process the command sent by the user.
* @param {WaterfallStepContext} step WaterfallStepContext
*/
async processStep(step) {
const tokenResponse = step.result;
const userLanguage = await this.languagePreferenceProperty.get(step.context, DEFAULT_LANGUAGE);
// If the user is authenticated the bot can use the token to make API calls.
if (tokenResponse !== undefined) {
let topIntent = false;
let results = null;
try{
let luis = this.luisRecognizer[userLanguage] || this.luisRecognizer[DEFAULT_LANGUAGE]
results = await this.luisRecognizer[userLanguage].recognize(step.context);
topIntent = LuisRecognizer.topIntent(results);
}catch(err){
console.error(`\n [onTurnError]: LUIS does not work. Only basic commands available. -> ${ err }`);
}
if(topIntent){
switch (topIntent){
case 'share-feedback':
await this.sendFeedbackMessage(step.context, userLanguage);
break;
default:
await Moodle.callMoodleWebservice(step.context, tokenResponse, topIntent, results.entities, userLanguage, this);
}
}else{
let parts = await this.commandState.get(step.context);
if (!parts) {
parts = step.context.activity.text;
}
parts = parts.split(' ');
let command = parts[0].toLowerCase();
command = command.trim();
if (command === 'help') {
await Moodle.callMoodleWebservice(step.context, tokenResponse, 'get-help', null, userLanguage, this);
} else if (command === 'feedback') {
await this.sendFeedbackMessage(step.context);
} else {
await step.context.sendActivity(this.translator[userLanguage].__("Sorry, I do not understand"));
}
}
} else {
// Ask the user to try logging in later as they are not logged in.
await step.context.sendActivity(this.translator[userLanguage].__("We couldn't log you in. Please try again later."));
}
return await step.endDialog();
};
async cacheBotData(storage, data){
let userObjectId = data.from.aadObjectId;
let userId = data.from.id;
let serviceUrl = data.serviceUrl;
let team = null;
if(data.channelData != undefined && data.channelData.team != undefined){
team = data.channelData.team.id;
}
try {
let storeItems = await storage.read(["botCache"])
var botCache = storeItems["botCache"];
if (typeof (botCache) != 'undefined') {
let store = false;
if (typeof (storeItems["botCache"].usersList[userObjectId]) == 'undefined') {
storeItems["botCache"].usersList[userObjectId] = userId;
store = true;
}
if(storeItems["botCache"].serviceUrl != serviceUrl){
storeItems["botCache"].serviceUrl = serviceUrl;
store = true;
}
if(team != null){
let teamslist = storeItems["botCache"].teamsList;
teamslist.push(team);
// Leaving only unique array values
storeItems["botCache"].teamsList = [...new Set(storeItems["botCache"].teamsList)];
}
if(store){
try {
await storage.write(storeItems)
} catch (err) {
console.log(`Write failed: ${err}`);
}
}
} else {
let botObject = data.recipient;
let tenantId = data.channelData.tenant.id;
let channelId = data.channelId;
let teamsList = [team];
let usersList = {};
usersList[userObjectId] = userId;
storeItems["botCache"] = { teamsList: teamsList, usersList: usersList, botObject: botObject,
tenant : tenantId, serviceUrl : serviceUrl, channelId : channelId, "eTag": "*" }
try {
await storage.write(storeItems)
} catch (err) {
console.log(`Write failed: ${err}`);
}
}
} catch (err) {
console.log(`Read rejected. ${err}`);
};
}
async getBotCacheData(storage, property){
let result = null;
let storeItems = await storage.read(["botCache"]);
if(storeItems["botCache"] != undefined){
result = storeItems["botCache"][property];
}
return result;
}
async getUserFromTeams(userObjectId, teams, connectorClient, storage){
let userid = null;
let storeItems = await storage.read(["botCache"])
let botCache = storeItems["botCache"];
let store = false;
for(let team of teams){
if(userid != null){
break;
}
let members = await connectorClient.conversations.getConversationMembersWithHttpOperationResponse(team);
members = JSON.parse(members.bodyAsText);
for(let member of members){
if (typeof (botCache) != 'undefined') {
if (typeof (storeItems["botCache"].usersList[member.objectId]) == 'undefined') {
storeItems["botCache"].usersList[member.objectId] = member.id;
store = true;
}
}
if(userObjectId == member.objectId){
userid = member.id;
break;
}
}
}
if(store){
try {
await storage.write(storeItems)
} catch (err) {
console.log(`Write failed of userslist: ${err}`);
}
}
return userid;
}
async processProactiveMessage(req, res, adapter, memoryStorage){
try{
parseRequest(req).then(async (data) => {
let userId = null;
const authHeader = req.headers.authorization || '';
data.serviceUrl = await this.getBotCacheData(memoryStorage, 'serviceUrl');
if(data.serviceUrl == null){
res.send('Bot cache empty');
res.status(404);
res.end();
}else{
data.channelId = await this.getBotCacheData(memoryStorage, 'channelId');
adapter.authenticateRequest(data, authHeader).then(async() => {
const connectorClient = await adapter.createConnectorClient(data.serviceUrl);
let usersList = await this.getBotCacheData(memoryStorage, 'usersList');
if(usersList[data.user] == undefined){
let teamslist = await this.getBotCacheData(memoryStorage, 'teamsList');
userId = await this.getUserFromTeams(data.user, teamslist, connectorClient, memoryStorage);
}else{
userId = usersList[data.user];
}
if(userId != null){
let tenantId = await this.getBotCacheData(memoryStorage, 'tenant');
const botparam = await this.getBotCacheData(memoryStorage, 'botObject');
const tenant = { id: tenantId };
const user = { id: userId };
const parameters = { bot: botparam, members: [user], channelData: {tenant: tenant}};
const newConversation = await connectorClient.conversations.createConversation(parameters);
const newReference = {
user: user,
bot: botparam,
conversation:
{ conversationType: 'personal',
id: newConversation.id },
channelId: data.channelId,
serviceUrl: data.serviceUrl
}
adapter.continueConversation(newReference, async (ctx) => {
const userLanguage = await this.languagePreferenceProperty.get(ctx, DEFAULT_LANGUAGE);
await Moodle.sendProactiveNotification(ctx, data, this.translator[userLanguage]);
res.send('Message sent');
res.status(200);
res.end();
});
}else{
res.send('User not found');
res.status(404);
res.end();
}
}, (reason) => {
res.send(reason);
res.status(401);
res.end();
});
}
});
}catch(err){
res.send(err);
res.status(500);
res.end();
}
}
};
// Check if language changes are requested
function isLanguageChangeRequested(newLanguage, currentLanguage) {
if (!newLanguage) {
return false;
}
const cleanedUpLanguage = newLanguage.toLowerCase().trim();
if (DEFAULT_AVAILABLE_LANGUAGES.indexOf(cleanedUpLanguage) == -1) {
return false;
}
return cleanedUpLanguage !== currentLanguage;
}
//sed to parse proactive notification content
function parseRequest(req) {
return new Promise((resolve, reject) => {
let requestData = '';
req.on('data', (chunk) => {
requestData += chunk;
});
req.on('end', () => {
try {
req.body = JSON.parse(requestData);
resolve(req.body);
}
catch (err) {
reject(err);
}
});
});
}
exports.MoodleWsBot = MoodleWsBot;
exports.DEFAULT_AVAILABLE_LANGUAGES = DEFAULT_AVAILABLE_LANGUAGES;

32
core/listcard.js Normal file
Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
class Listcard {
static createListCard(title = '', items = [], buttons = []){
let data = {
contentType : "application/vnd.microsoft.teams.card.list",
content : {
title : title,
items : items,
buttons : buttons
}
}
return data;
}
static createListCardItem(cardtype, title = '', subtitle = '', icon = null, action = null, other = null){
let data = {
type: cardtype,
icon: icon,
title: title,
subtitle: subtitle,
tap: action
};
for(let param in other){
data[param] = other[param];
}
return data;
}
}
exports.Listcard = Listcard;

27
core/login.js Normal file
Просмотреть файл

@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { OAuthPrompt } = require('botbuilder-dialogs');
// DialogId for the OAuthPrompt.
const LOGIN_PROMPT = 'loginPrompt';
class Login {
/**
* Prompts the user to log in using the OAuth provider specified by the connection name.
* @param {string} connectionName The connectionName from Azure when the OAuth provider is created.
*/
static prompt(connectionName, translator) {
const loginPrompt = new OAuthPrompt(LOGIN_PROMPT,
{
connectionName: connectionName,
text: translator.__('Please login'),
title: 'Login',
timeout: 30000 // User has 5 minutes to login.
});
return loginPrompt;
}
}
exports.Login = Login;
exports.LOGIN_PROMPT = LOGIN_PROMPT;

137
core/moodle.js Normal file
Просмотреть файл

@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ActivityTypes, TokenResponse, TurnContext, MessageFactory, CardFactory } = require('botbuilder');
const { SimpleGraphClient } = require('./../services/simple-graph-client');
const { SimpleMoodleClient } = require('./../services/simple-moodle-client');
const { Listcard } = require('./listcard');
// Moodle instance url
const MOODLE_URL = process.env.moodleUrl || false;
if(!MOODLE_URL) {
console.error("Please set up environment variable 'moodleUrl'");
process.exit(1);
}
class Moodle {
/**
* Displays available questions for users.
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
* @param {TokenResponse} tokenResponse A response that includes a user token.
* @param {string} intent Question intent.
* @param {any} entities Intent data.
* @param {string} userLanguage User language in which answer should be returned.
*/
static async callMoodleWebservice(turnContext, tokenResponse, intent, entities = {}, userLanguage = DEFAULT_LANGUAGE, bot = false) {
if (!turnContext) {
throw new Error('Moodle.callMoodleWebservice(): `turnContext` cannot be undefined.');
}
if (!tokenResponse) {
throw new Error('Moodle.callMoodleWebservice(): `tokenResponse` cannot be undefined.');
}
if (!intent) {
throw new Error('Moodle.callMoodleWebservice(): `intent` cannot be undefined.');
}
const client = new SimpleGraphClient(tokenResponse.token);
const me = await client.getMe();
if(me.exception){
console.log('Moodle.callMoodleWebservice(): error occured -> ' + data.message);
await turnContext.sendActivity(bot.translator[userLanguage].__("Sorry, the answer to this question is not available for now"));
}
// Get message from Moodle.
const moodleclient = new SimpleMoodleClient(MOODLE_URL, me.mail, tokenResponse.token);
entities = JSON.stringify(entities);
const data = await moodleclient.get_moodle_reply(intent, entities);
if(data.language && bot){
let language = data.language.split('_');
let languagechanged = await bot.changeLanguage(turnContext, language[0], userLanguage);
}
if(data.exception){
console.log('Moodle.callMoodleWebservice(): error occured -> ' + data.message);
await turnContext.sendActivity(bot.translator[userLanguage].__("Sorry, the answer to this question is not available for now"));
} else if((data.listItems && data.listItems.length > 0) || data.message != ''){
await this.sendMoodleReply(turnContext, data);
}else{
await turnContext.sendActivity(bot.translator[userLanguage].__("Sorry, I do not understand"));
}
}
/**
* Sends message for user
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation.
* @param data Message data.
* @param {boolean} activityFeed If set true, message will be showed in Teams activity feed.
*/
static async sendMoodleReply(turnContext, data, activityFeed = false) {
if(data.listItems && data.listItems.length > 0){
let cardListItems = [];
for(let item of data.listItems){
let action = null;
if(item.action || item.url){
item.actionType = item.actionType || 'openUrl';
item.action = item.action || item.url;
action = {
type: item.actionType,
value: item.action
}
}
cardListItems.push(
Listcard.createListCardItem(
'resultItem',
item.title,
item.subtitle,
item.icon,
action
)
);
}
let listCard = Listcard.createListCard(data.listTitle, cardListItems);
let messageWithCard = MessageFactory.list([listCard], data.message);
if(activityFeed){
messageWithCard.channelData = {notification: {alert: true}};
}
await turnContext.sendActivity(messageWithCard);
}else{
let proactivenotification = { type: ActivityTypes.Message };
proactivenotification.text = data.message;
if(activityFeed){
proactivenotification.channelData = {notification: {alert: true}};
}
await turnContext.sendActivity(proactivenotification);
}
}
/**
* Sends proactive notification for user
* @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation.
* @param data Message data.
*/
static async sendProactiveNotification(turnContext, data, usertranslator) {
if(data.listItems && data.listItems.length == 1){
let item = data.listItems[0];
item.actionType = item.actionType || 'openUrl';
item.action = item.action || item.url;
let actions = null;
if(item.action){
actions = CardFactory.actions([
{
type: item.actionType,
title: usertranslator.__("View"),
value: item.action
}
]);
}
let icon = null;
if(item.icon){
icon = [{ url: item.icon }];
}
const messageCard = CardFactory.thumbnailCard(item.title, icon, actions, {text: data.message});
await turnContext.sendActivity({ attachments: [messageCard], channelData: { notification: { alert: true } } });
} else {
await this.sendMoodleReply(turnContext, data, true);
}
}
}
exports.Moodle = Moodle;

133
deploy.cmd Normal file
Просмотреть файл

@ -0,0 +1,133 @@
@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off
:: ----------------------
:: KUDU Deployment Script
:: Version: 1.0.17
:: ----------------------
:: Prerequisites
:: -------------
:: Verify node.js installed
where node 2>nul >nul
IF %ERRORLEVEL% NEQ 0 (
echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment.
goto error
)
:: Setup
:: -----
setlocal enabledelayedexpansion
SET ARTIFACTS=%~dp0%..\artifacts
IF NOT DEFINED DEPLOYMENT_SOURCE (
SET DEPLOYMENT_SOURCE=%~dp0%.
)
IF NOT DEFINED DEPLOYMENT_TARGET (
SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot
)
IF NOT DEFINED NEXT_MANIFEST_PATH (
SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest
IF NOT DEFINED PREVIOUS_MANIFEST_PATH (
SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest
)
)
IF NOT DEFINED KUDU_SYNC_CMD (
:: Install kudu sync
echo Installing Kudu Sync
call npm install kudusync -g --silent
IF !ERRORLEVEL! NEQ 0 goto error
:: Locally just running "kuduSync" would also work
SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd
)
goto Deployment
:: Utility Functions
:: -----------------
:SelectNodeVersion
IF DEFINED KUDU_SELECT_NODE_VERSION_CMD (
:: The following are done only on Windows Azure Websites environment
call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%"
IF !ERRORLEVEL! NEQ 0 goto error
IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" (
SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp"
IF !ERRORLEVEL! NEQ 0 goto error
)
IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" (
SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp"
IF !ERRORLEVEL! NEQ 0 goto error
)
IF NOT DEFINED NODE_EXE (
SET NODE_EXE=node
)
SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!"
) ELSE (
SET NPM_CMD=npm
SET NODE_EXE=node
)
goto :EOF
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------
:Deployment
echo Handling node.js deployment.
:: 1. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
IF !ERRORLEVEL! NEQ 0 goto error
)
:: 2. Select node version
call :SelectNodeVersion
:: 3. Install npm packages
IF EXIST "%DEPLOYMENT_TARGET%\package.json" (
pushd "%DEPLOYMENT_TARGET%"
call :ExecuteCmd !NPM_CMD! install --production
IF !ERRORLEVEL! NEQ 0 goto error
popd
)
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto end
:: Execute command routine that will echo out when error
:ExecuteCmd
setlocal
set _CMD_=%*
call %_CMD_%
if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_%
exit /b %ERRORLEVEL%
:error
endlocal
echo An error has occurred during web site deployment.
call :exitSetErrorLevel
call :exitFromFunction 2>nul
:exitSetErrorLevel
exit /b 1
:exitFromFunction
()
:end
endlocal
echo Finished successfully.

1
iisnode.yml Normal file
Просмотреть файл

@ -0,0 +1 @@
nodeProcessCommandLine: "D:\Program Files (x86)\nodejs\8.9.4\node.exe"

72
index.js Normal file
Просмотреть файл

@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Import required packages
const restify = require('restify');
const Translator = require('i18n-nodejs');
// Import required bot services.
const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder');
const { MoodleWsBot, DEFAULT_AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE } = require('./core/bot');
if(!process.env.microsoftAppID) {
console.error("Please set up environment variable 'microsoftAppID'");
process.exit(1);
}
if(!process.env.microsoftAppPassword) {
console.error("Please set up environment variable 'microsoftAppPassword'");
process.exit(1);
}
let translator = [];
for (let lang of DEFAULT_AVAILABLE_LANGUAGES){
translator[lang] = new Translator(lang, "./../../lang/translations.json");
}
// Create bot adapter.
const adapter = new BotFrameworkAdapter({
appId: process.env.microsoftAppID,
appPassword: process.env.microsoftAppPassword,
openIdMetadata: process.env.BotOpenIdMetadata
});
// Catch-all for any unhandled errors in bot.
adapter.onTurnError = async (context, error) => {
console.error(`\n [onTurnError]: ${ error }`);
// Send a message to the user
if(error.code == "InvalidAuthenticationToken"){
await context.sendActivity(translator[DEFAULT_LANGUAGE].__("Your session has timed out."));
}else{
await context.sendActivity(translator[DEFAULT_LANGUAGE].__("Oops. Something went wrong!"));
}
// Clear out state
conversationState.clear(context);
};
// Define a state store for your bot.
const memoryStorage = new MemoryStorage();
let conversationState = new ConversationState(memoryStorage);
let userState = new UserState(memoryStorage);
// Create the main dialog.
const bot = new MoodleWsBot(conversationState, userState, translator);
// Create HTTP server
let server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log(`\n${ server.name } listening to ${ server.url }`);
});
// Listen for incoming activities and route them to bot main dialog.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
bot.cacheBotData(memoryStorage, context.activity);
// route to main dialog.
await bot.onTurn(context);
});
});
// Listen for proactive notifications and send them for users.
server.post('/api/webhook', async (req, res) => {
bot.processProactiveMessage(req, res, adapter, memoryStorage);
});

58
lang/translations.json Normal file
Просмотреть файл

@ -0,0 +1,58 @@
{
"Give feedback": {
"es": "Dar opinion",
"pt": "Dê retorno"
},
"Hello!":{
"es": "¡Hola!",
"pt": "Olá!"
},
"Help":{
"es": "Ayuda",
"pt": "Socorro"
},
"I am Moodle Assistant, a bot that answers questions about your assignments and courses. <br/><br/> If you are curious about what I can do, just type 'help' or click on the button below and I will give you the list of questions I can answer!": {
"es": "Soy Moodle Assistant, un robot que responde preguntas sobre tus tareas y cursos. <br/> <br/> Si tiene curiosidad sobre lo que puedo hacer, simplemente escriba 'ayuda' o haga clic en el botón de abajo y le daré la lista de preguntas que puedo responder.",
"pt": "Eu sou o Moodle Assistant, um bot que responde a perguntas sobre suas tarefas e cursos. <br/> <br/> Se você está curioso sobre o que eu posso fazer, basta digitar 'help' ou clicar no botão abaixo e eu lhe darei a lista de perguntas que posso responder!"
},
"Oops. Something went wrong!": {
"es": "Ups. ¡Algo salió mal!",
"pt": "Oops Algo deu errado!"
},
"Please give us feedback by clicking on the button below.":{
"es": "Por favor denos su opinión haciendo clic en el botón de abajo.",
"pt": "Por favor, nos dê seu feedback clicando no botão abaixo."
},
"Please login": {
"es": "Por favor Iniciar sesión",
"pt": "Por favor entre"
},
"Sorry, I do not understand": {
"es": "Lo siento, no entiendo",
"pt": "Desculpe me, eu não entendo"
},
"Sorry, the answer to this question is not available for now":{
"es": "Lo sentimos, la respuesta a esta pregunta no está disponible por ahora.",
"pt": "Desculpe, a resposta a esta pergunta não está disponível por enquanto"
},
"The answer to your query can not be displayed in team conversation. Please ask me the same question in personal chat.": {
"es": "La respuesta a su consulta no se puede mostrar en una conversación de equipo. Por favor, pregúntame la misma pregunta en el chat personal.",
"pt": "A resposta à sua consulta não pode ser exibida na conversa da equipe. Por favor, faça-me a mesma pergunta no chat pessoal."
},
"We couldn't log you in. Please try again later.": {
"es": "No pudimos iniciar sesión. Vuelve a intentarlo más tarde.",
"pt": "Não foi possível fazer o login. Por favor, tente novamente mais tarde."
},
"View": {
"es": "Vista",
"pt": "Visão"
},
"You are now signed out.": {
"es": "Ahora estás desconectado.",
"pt": "Você está desconectado agora."
},
"Your session has timed out.": {
"es": "Tu sesión ha expirado.",
"pt": "Sua sessão expirou."
}
}

35
package.json Normal file
Просмотреть файл

@ -0,0 +1,35 @@
{
"name": "Moodle",
"version": "1.0.0",
"description": "Bot Builder v4 Moodle bot",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"watch": "nodemon index.js"
},
"author": "Enovation",
"license": "MIT",
"dependencies": {
"@microsoft/microsoft-graph-client": "^1.2.0",
"botbuilder": "4.0.6",
"botbuilder-ai": "4.0.6",
"botbuilder-dialogs": "4.0.6",
"botframework-connector": "4.0.6",
"i18n-nodejs": "^3.0.0",
"moodle-client": "^0.5.2",
"promise-retry" : "1.1.1",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"restify": "^6.3.4"
},
"devDependencies": {
"eslint": "^5.6.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"nodemon": "^1.18.4"
}
}

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { Client } = require('@microsoft/microsoft-graph-client');
let promiseRetry = require("promise-retry");
/**
* This class is a wrapper for the Microsoft Graph API.
* See: https://developer.microsoft.com/en-us/graph for more information.
*/
class SimpleGraphClient {
constructor(token) {
if (!token || !token.trim()) {
throw new Error('SimpleGraphClient: Invalid token received.');
}
this._token = token;
// Get an Authenticated Microsoft Graph client using the token issued to the user.
this.graphClient = Client.init({
authProvider: (done) => {
done(null, this._token); // First parameter takes an error if you can't get an access token.
}
});
}
/**
* Collects information about the user in the bot.
*/
async getMe() {
let client = await this.graphClient;
return promiseRetry(function (retry) {
return client.api('/me')
.get()
.catch(retry);
})
.then(function (response) {
return response;
}, function (err) {
console.log("Unable to get data from Graph API: " + err);
return {exception: 1, message: 'Graph API not working'};
});
}
}
exports.SimpleGraphClient = SimpleGraphClient;

Просмотреть файл

@ -0,0 +1,69 @@
let moodleClient = require("moodle-client");
let rp = require("request-promise");
let promiseRetry = require("promise-retry");
/**
* This class is a wrapper for the Moodle webservices.
*/
class SimpleMoodleClient {
constructor(url, email, token) {
if (!url || !url.trim()) {
throw new Error('MoodleClient: Invalid url received.');
}
if (!email || !email.trim()) {
throw new Error('MoodleClient: Invalid email received.');
}
if (!token || !token.trim()) {
throw new Error('MoodleClient: Invalid token received.');
}
let options = {
uri: `${ url }/local/o365/token.php`,
qs: {
'username': email,
'service': 'o365_webservices',
},
headers: {
'Authorization': 'Bearer '+ token
},
json: true
};
this.moodleWS = promiseRetry(function (retry) {
return rp(options)
.catch(retry);
})
.then(function (response) {
return moodleClient.init({
wwwroot: url,
token: response.token,
service: 'o365_webservices'
});
}, function (err) {
console.log("Unable to initialize the Moodle client: " + err);
});
}
async get_moodle_reply(intent, entities = null) {
return await this.moodleWS.then(function(client){
return promiseRetry(function (retry) {
return client.call({
wsfunction: "local_o365_get_bot_message",
args: {
intent: intent,
entities: entities
}
})
.catch(retry);
})
.then(function (response) {
return response;
}, function (err) {
console.log("Unable to get data from Moodle WS: " + err);
return {exception: 1, message: 'WS not working'};
});
});
}
}
exports.SimpleMoodleClient = SimpleMoodleClient;

61
web.config Normal file
Просмотреть файл

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This configuration file is required if iisnode is used to run node processes behind
IIS or IIS Express. For more information, visit:
https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->
<configuration>
<system.webServer>
<!-- Visit http://blogs.msdn.com/b/windowsazure/archive/2013/11/14/introduction-to-websockets-on-windows-azure-web-sites.aspx for more information on WebSocket support -->
<webSocket enabled="false" />
<handlers>
<!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
<add name="iisnode" path="index.js" verb="*" modules="iisnode"/>
</handlers>
<rewrite>
<rules>
<!-- Do not interfere with requests for node-inspector debugging -->
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^index.js\/debug[\/]?" />
</rule>
<!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
<rule name="StaticContent">
<action type="Rewrite" url="public{REQUEST_URI}"/>
</rule>
<!-- All other URLs are mapped to the node.js site entry point -->
<rule name="DynamicContent">
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
</conditions>
<action type="Rewrite" url="index.js"/>
</rule>
</rules>
</rewrite>
<!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
<security>
<requestFiltering>
<hiddenSegments>
<remove segment="bin"/>
</hiddenSegments>
</requestFiltering>
</security>
<!-- Make sure error responses are left untouched -->
<httpErrors existingResponse="PassThrough" />
<!--
You can control how Node is hosted within IIS using the following options:
* watchedFiles: semi-colon separated list of files that will be watched for changes to restart the server
* node_env: will be propagated to node as NODE_ENV environment variable
* debuggingEnabled - controls whether the built-in debugger is enabled
See https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config for a full list of options
-->
<!--<iisnode watchedFiles="web.config;*.js"/>-->
</system.webServer>
</configuration>