Added bot code
This commit is contained in:
Родитель
744e878b5b
Коммит
b967084d63
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
nodeProcessCommandLine: "D:\Program Files (x86)\nodejs\8.9.4\node.exe"
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
Загрузка…
Ссылка в новой задаче