├── img ├── Architecture.pptx └── architecture.png ├── package.json ├── LICENSE ├── .gitignore ├── src ├── app.ts └── msgraphService.ts ├── tsconfig.json └── README.md /img/Architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NT-D/RoomFinder/HEAD/img/Architecture.pptx -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NT-D/RoomFinder/HEAD/img/architecture.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roomfinder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "watch": "node ./node_modules/typescript/bin/tsc --watch", 9 | "start": "node dist/app.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/NT-D/RoomFinder.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/NT-D/RoomFinder/issues" 19 | }, 20 | "homepage": "https://github.com/NT-D/RoomFinder#readme", 21 | "dependencies": { 22 | "@microsoft/microsoft-graph-types": "^1.1.0", 23 | "@types/actions-on-google": "^1.8.0", 24 | "@types/body-parser": "^1.16.8", 25 | "@types/express": "^4.11.1", 26 | "@types/node-fetch": "^1.6.7", 27 | "actions-on-google": "^1.10.0", 28 | "body-parser": "^1.18.2", 29 | "express": "^4.16.2", 30 | "node-fetch": "^2.1.1", 31 | "npm": "^5.7.1", 32 | "typescript": "^2.7.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Masayuki Ota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Typescript dist files 61 | dist 62 | 63 | # VS Code debug configuration 64 | .vscode -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { DialogflowApp } from 'actions-on-google'; 4 | import { getUserInfo, reserveRoom } from './msgraphService'; 5 | import msGraph from '@microsoft/microsoft-graph-types'; 6 | 7 | const bookRoomActionName = 'reserve.room'; 8 | const communicationName = 'communication'; 9 | 10 | const expressApp = express(); 11 | expressApp.use(bodyParser.json()); 12 | 13 | expressApp.post('/gghome', (req, res) => { 14 | const dialogApp = new DialogflowApp({ 15 | request: req, 16 | response: res 17 | }); 18 | 19 | function bookRoom() { 20 | reserveRoom(dialogApp.getUser().accessToken) 21 | .then(result => { 22 | console.log(result); 23 | if (result.location && result.location.displayName) dialogApp.ask(`${result.location.displayName}を1時間確保しました!`); 24 | dialogApp.ask("確保しました!"); 25 | }) 26 | .catch(err => { 27 | console.log(err); 28 | dialogApp.ask("Error発生!ちゃんとハンドルしよう"); 29 | }); 30 | } 31 | 32 | function communication() { 33 | getUserInfo(dialogApp.getUser().accessToken) 34 | .then(result => { 35 | console.log(result.surname); 36 | dialogApp.ask("テスト"); 37 | }); 38 | } 39 | 40 | let actionMap = new Map(); 41 | actionMap.set(bookRoomActionName, bookRoom); 42 | actionMap.set(communicationName,communication); 43 | dialogApp.handleRequest(actionMap); 44 | }); 45 | 46 | expressApp.listen(process.env.PORT || 8080); -------------------------------------------------------------------------------- /src/msgraphService.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import msGraph from '@microsoft/microsoft-graph-types'; 3 | 4 | const apiEndpointUrl: string = 'https://graph.microsoft.com/v1.0/'; 5 | const timezone: string = 'Tokyo Standard Time'; 6 | const meetingRoomLists: string[] = ['meetingroom1@sample.com', 'meetingroom2@sample.com']; 7 | 8 | export async function reserveRoom(accessToken: string): Promise { 9 | //Set Date 10 | const start: Date = new Date(); 11 | start.setHours(start.getHours() + 9);//TODO; set appropriate timezone value (Now I set +9 for Japan demo) 12 | let startTime: string = start.toISOString(); 13 | startTime = startTime.substring(0, startTime.indexOf(".")); 14 | start.setHours(start.getHours() + 1); 15 | let endTime: string = start.toISOString(); 16 | endTime = endTime.substring(0, endTime.indexOf(".")); 17 | 18 | //Create header 19 | const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'Prefer': `outlook.timezone="${timezone}"` }; 20 | 21 | //Construct body 22 | let meetingInfo: msGraph.Event = { 23 | subject: "Casual meeting booked by Google Home", 24 | start: { dateTime: startTime, timeZone: timezone }, 25 | end: { dateTime: endTime, timeZone: timezone }, 26 | location: { locationEmailAddress: meetingRoomLists[1],displayName:"19F meeting room" }, 27 | attendees: [{ emailAddress: { address: meetingRoomLists[1] }, type: "resource" }], 28 | body: { content: "Reserved by Google Home and Masayuki Ota" } 29 | }; 30 | 31 | //Send request 32 | const response = await fetch(`${apiEndpointUrl}/me/calendar/events`, { method: "POST", headers: headers, body: JSON.stringify(meetingInfo) }); 33 | return await response.json() as msGraph.Event; 34 | } 35 | 36 | async function getRoomStatus(roomAlias: string): Promise { 37 | let status = false; 38 | //TODO:Will implement 39 | return status; 40 | } 41 | 42 | export async function getUserInfo(accessToken: string): Promise { 43 | const response = await fetch(`${apiEndpointUrl}/me`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); 44 | return await response.json() as msGraph.User; 45 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./dist", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RoomFinder 2 | This is demo project for booking meeting room in Office 365 Tenant by Google Assistant (Home). 3 | 4 | ## What you can learn 5 | - How to setup OAuth authentication for [Microsoft Graph](https://developer.microsoft.com/en-us/graph) in [Dialog Flow](https://dialogflow.com/) 6 | - How to book meeting room through Microsoft Graph 7 | 8 | ## Current version limitation 9 | - Haven't implemented the function to find available room in room list. You can implement it with following 2 APIs. 10 | 1. Get meeting rooms with [findRoomLists API](https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/user_findroomlists) and [findRooms API](https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/user_findrooms). 11 | 2. Find available foom with [findMeetingTimes API](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_findmeetingtimes) or [List events API](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_events) 12 | - No test.. sorry 13 | 14 | # Solution description 15 | ## Current Architecture 16 | ![architecture](./img/architecture.png) 17 | 18 | ## Prerequisites 19 | - Install [Node.js](https://nodejs.org/en/) in your development environment. I assume you can understand es6. 20 | - Basic TypeScript knowledge. Please learn from [tutorial document](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) 21 | - I assume you have O365 tenant. If you want to try O365, you can try trhough [Free trial] in [product page](https://products.office.com/en/business/office-365-enterprise-e3-business-software) 22 | - In addition to it, I assume you have meeting room as resource mail box in O365 tenant. If you dont't have it, you can make it with [this document](https://support.office.com/en-us/article/room-and-equipment-mailboxes-9f518a6d-1e2c-4d44-93f3-e19013a1552b#ID0EABAAA=Set_it_up) 23 | 24 | ## How to setup 25 | For OAuth authentication, need to register app and pickup `Client ID` and `Client Secret`. In addition to it, need to set redirect url and scope. You can learn how to setup with following section. 26 | 27 | ### Register app in Microsoft App registration portal 28 | 1. Register(create) new app in the [app registration portal](https://apps.dev.microsoft.com/) with [this document](https://developer.microsoft.com/en-us/graph/docs/concepts/auth_register_app_v2). Please save your `Applicaion Id(Client ID)` and `Password(Client Secret)` in the clipboard. 29 | 2. Add platform as `Web` with [Add Platform]button. 30 | 3. Paste `https://oauth-redirect.googleusercontent.com/r/` in the [Redirect URLs]text box. 31 | 4. Add `Calendars.ReadWrite.Shared` and `User.Read` delegated permissions in the [Microsoft Graph Permissions]section. 32 | 5. Save your change. 33 | 34 | ### Setup Dialog flow (Basic) 35 | - [Play around to build your first agent](https://dialogflow.com/docs/getting-started/building-your-first-agent) and try to [use Fulfillment](https://dialogflow.com/docs/getting-started/basic-fulfillment-conversation). You will update Webhook url later. 36 | - Please set `reserve.room` as action name. It need to support Fulfillment. 37 | 38 | 39 | ### Setup Dialog flow (Authentication) 40 | [Implementing Account Linking](https://developers.google.com/actions/identity/account-linking) with following parameters. 41 | 42 | |Property name|Value| 43 | |:---|:---| 44 | |Grant type|Authorization code| 45 | |Client ID|Application Id generated in app registration portal| 46 | |Client secret|Password generated in app registration portal| 47 | |Authorization URL|https://login.microsoftonline.com/common/oauth2/v2.0/authorize| 48 | |Token URL |https://login.microsoftonline.com/common/oauth2/v2.0/token| 49 | |Scopes|https://graph.microsoft.com/Calendars.ReadWrite.Shared https://graph.microsoft.com/User.Read| 50 | 51 | *Important*: You need to turn on [Sign in required] in the [Integration menu] in [dialog flow console](https://console.dialogflow.com). 52 | 53 | ## How to run 54 | 1. `git clone https://github.com/NT-D/RoomFinder.git` 55 | 2. `npm install` in terminal(or command prompt) for installing node modules 56 | 3. `npm run watch` in another terminal for trans-complie .ts to .js file 57 | 4. `npm run start` in terminal for starting app. 58 | 5. Setup [ngrok](https://ngrok.com/) and create forwarding url (ex. You will get the url like https://c349cad0.ngrok.io) 59 | 6. [Set previous url in the Fulfillment settings](https://dialogflow.com/docs/getting-started/basic-fulfillment-conversation#enable_webhook_in_dialogflow) in dialog flow. 60 | 61 | ## Implementation description 62 | ### How to get access token 63 | You can get access token through this code in `app.ts`. 64 | ```javascript 65 | dialogApp.getUser().accessToken 66 | ``` 67 | 68 | ### Call MS Graph by app 69 | You can call graph API like this code in `msgraphService.ts`. 70 | ```javascript 71 | export async function getUserInfo(accessToken: string): Promise { 72 | const response = await fetch(`${apiEndpointUrl}/me`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); 73 | return await response.json() as msGraph.User; 74 | } 75 | ``` 76 | 77 | # Userful resources 78 | - [Samples and Libraries for Actions on Google](https://github.com/actions-on-google) 79 | - [Get started with Microsoft Graph in a Node.js app](https://developer.microsoft.com/en-us/graph/docs/concepts/nodejs) 80 | - [MS Graph Types](https://github.com/microsoftgraph/msgraph-typescript-typings) 81 | - [Time zone lists](https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones) 82 | - [Get started with Microsoft Graph in a Node.js app](https://developer.microsoft.com/en-us/graph/docs/concepts/nodejs) 83 | - [Create Event in MS Graph](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_post_events#request-headers) --------------------------------------------------------------------------------