├── .gitignore
├── ApprovalBot.sln
├── ApprovalBot
├── App_Start
│ └── WebApiConfig.cs
├── ApprovalBot.csproj
├── Controllers
│ ├── MessagesController.cs
│ └── ResponsesController.cs
├── Dialogs
│ ├── ExceptionHandlerDialog.cs
│ └── RootDialog.cs
├── Global.asax
├── Global.asax.cs
├── Helpers
│ ├── ApprovalRequestHelper.cs
│ ├── ApprovalStatusHelper.cs
│ ├── DatabaseHelper.cs
│ ├── EmailHelper.cs
│ ├── GraphHelper.cs
│ └── TimeZoneHelper.cs
├── Models
│ ├── ActionData.cs
│ ├── ActionableEmailResponse.cs
│ ├── Approval.cs
│ ├── ApproverInfo.cs
│ └── FileInfo.cs
├── OutlookAdaptive
│ ├── AdaptiveActionSet.cs
│ ├── AdaptiveHttpAction.cs
│ └── AdaptiveToggleVisibilityAction.cs
├── PrivateSettings.example.config
├── Properties
│ └── AssemblyInfo.cs
├── Web.Debug.config
├── Web.Release.config
├── Web.config
├── default.htm
└── packages.config
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── SECURITY.md
└── readme-images
├── aad-application-id.PNG
├── aad-copy-client-secret.png
├── aad-new-client-secret.png
├── aad-portal-app-registrations.png
├── aad-register-an-app.PNG
├── configure-emulator.PNG
├── hello-bot.PNG
├── ngrok1.PNG
└── ngrok2.PNG
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # Benchmark Results
46 | BenchmarkDotNet.Artifacts/
47 |
48 | # .NET Core
49 | project.lock.json
50 | project.fragment.lock.json
51 | artifacts/
52 | **/Properties/launchSettings.json
53 |
54 | *_i.c
55 | *_p.c
56 | *_i.h
57 | *.ilk
58 | *.meta
59 | *.obj
60 | *.pch
61 | *.pdb
62 | *.pgc
63 | *.pgd
64 | *.rsp
65 | *.sbr
66 | *.tlb
67 | *.tli
68 | *.tlh
69 | *.tmp
70 | *.tmp_proj
71 | *.log
72 | *.vspscc
73 | *.vssscc
74 | .builds
75 | *.pidb
76 | *.svclog
77 | *.scc
78 |
79 | # Chutzpah Test files
80 | _Chutzpah*
81 |
82 | # Visual C++ cache files
83 | ipch/
84 | *.aps
85 | *.ncb
86 | *.opendb
87 | *.opensdf
88 | *.sdf
89 | *.cachefile
90 | *.VC.db
91 | *.VC.VC.opendb
92 |
93 | # Visual Studio profiler
94 | *.psess
95 | *.vsp
96 | *.vspx
97 | *.sap
98 |
99 | # TFS 2012 Local Workspace
100 | $tf/
101 |
102 | # Guidance Automation Toolkit
103 | *.gpState
104 |
105 | # ReSharper is a .NET coding add-in
106 | _ReSharper*/
107 | *.[Rr]e[Ss]harper
108 | *.DotSettings.user
109 |
110 | # JustCode is a .NET coding add-in
111 | .JustCode
112 |
113 | # TeamCity is a build add-in
114 | _TeamCity*
115 |
116 | # DotCover is a Code Coverage Tool
117 | *.dotCover
118 |
119 | # AxoCover is a Code Coverage Tool
120 | .axoCover/*
121 | !.axoCover/settings.json
122 |
123 | # Visual Studio code coverage results
124 | *.coverage
125 | *.coveragexml
126 |
127 | # NCrunch
128 | _NCrunch_*
129 | .*crunch*.local.xml
130 | nCrunchTemp_*
131 |
132 | # MightyMoose
133 | *.mm.*
134 | AutoTest.Net/
135 |
136 | # Web workbench (sass)
137 | .sass-cache/
138 |
139 | # Installshield output folder
140 | [Ee]xpress/
141 |
142 | # DocProject is a documentation generator add-in
143 | DocProject/buildhelp/
144 | DocProject/Help/*.HxT
145 | DocProject/Help/*.HxC
146 | DocProject/Help/*.hhc
147 | DocProject/Help/*.hhk
148 | DocProject/Help/*.hhp
149 | DocProject/Help/Html2
150 | DocProject/Help/html
151 |
152 | # Click-Once directory
153 | publish/
154 |
155 | # Publish Web Output
156 | *.[Pp]ublish.xml
157 | *.azurePubxml
158 | # Note: Comment the next line if you want to checkin your web deploy settings,
159 | # but database connection strings (with potential passwords) will be unencrypted
160 | *.pubxml
161 | *.publishproj
162 |
163 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
164 | # checkin your Azure Web App publish settings, but sensitive information contained
165 | # in these scripts will be unencrypted
166 | PublishScripts/
167 |
168 | # NuGet Packages
169 | *.nupkg
170 | # The packages folder can be ignored because of Package Restore
171 | **/packages/*
172 | # except build/, which is used as an MSBuild target.
173 | !**/packages/build/
174 | # Uncomment if necessary however generally it will be regenerated when needed
175 | #!**/packages/repositories.config
176 | # NuGet v3's project.json files produces more ignorable files
177 | *.nuget.props
178 | *.nuget.targets
179 |
180 | # Microsoft Azure Build Output
181 | csx/
182 | *.build.csdef
183 |
184 | # Microsoft Azure Emulator
185 | ecf/
186 | rcf/
187 |
188 | # Windows Store app package directories and files
189 | AppPackages/
190 | BundleArtifacts/
191 | Package.StoreAssociation.xml
192 | _pkginfo.txt
193 | *.appx
194 |
195 | # Visual Studio cache files
196 | # files ending in .cache can be ignored
197 | *.[Cc]ache
198 | # but keep track of directories ending in .cache
199 | !*.[Cc]ache/
200 |
201 | # Others
202 | ClientBin/
203 | ~$*
204 | *~
205 | *.dbmdl
206 | *.dbproj.schemaview
207 | *.jfm
208 | *.pfx
209 | *.publishsettings
210 | orleans.codegen.cs
211 |
212 | # Since there are multiple workflows, uncomment next line to ignore bower_components
213 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
214 | #bower_components/
215 |
216 | # RIA/Silverlight projects
217 | Generated_Code/
218 |
219 | # Backup & report files from converting an old project file
220 | # to a newer Visual Studio version. Backup files are not needed,
221 | # because we have git ;-)
222 | _UpgradeReport_Files/
223 | Backup*/
224 | UpgradeLog*.XML
225 | UpgradeLog*.htm
226 |
227 | # SQL Server files
228 | *.mdf
229 | *.ldf
230 | *.ndf
231 |
232 | # Business Intelligence projects
233 | *.rdl.data
234 | *.bim.layout
235 | *.bim_*.settings
236 |
237 | # Microsoft Fakes
238 | FakesAssemblies/
239 |
240 | # GhostDoc plugin setting file
241 | *.GhostDoc.xml
242 |
243 | # Node.js Tools for Visual Studio
244 | .ntvs_analysis.dat
245 | node_modules/
246 |
247 | # Typescript v1 declaration files
248 | typings/
249 |
250 | # Visual Studio 6 build log
251 | *.plg
252 |
253 | # Visual Studio 6 workspace options file
254 | *.opt
255 |
256 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
257 | *.vbw
258 |
259 | # Visual Studio LightSwitch build output
260 | **/*.HTMLClient/GeneratedArtifacts
261 | **/*.DesktopClient/GeneratedArtifacts
262 | **/*.DesktopClient/ModelManifest.xml
263 | **/*.Server/GeneratedArtifacts
264 | **/*.Server/ModelManifest.xml
265 | _Pvt_Extensions
266 |
267 | # Paket dependency manager
268 | .paket/paket.exe
269 | paket-files/
270 |
271 | # FAKE - F# Make
272 | .fake/
273 |
274 | # JetBrains Rider
275 | .idea/
276 | *.sln.iml
277 |
278 | # CodeRush
279 | .cr/
280 |
281 | # Python Tools for Visual Studio (PTVS)
282 | __pycache__/
283 | *.pyc
284 |
285 | # Cake - Uncomment if you are using it
286 | # tools/**
287 | # !tools/packages.config
288 |
289 | # Tabs Studio
290 | *.tss
291 |
292 | # Telerik's JustMock configuration file
293 | *.jmconfig
294 |
295 | # BizTalk build output
296 | *.btp.cs
297 | *.btm.cs
298 | *.odx.cs
299 | *.xsd.cs
300 | ApprovalBot/PrivateSettings.config
301 | demo/
302 | ApprovalBot/PrivateSettings.local.config
303 | ApprovalBot/PrivateSettings.azure.config
304 |
--------------------------------------------------------------------------------
/ApprovalBot.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2037
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApprovalBot", "ApprovalBot\ApprovalBot.csproj", "{4E426B56-D589-44EA-B7A1-72ED5343B354}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {4E426B56-D589-44EA-B7A1-72ED5343B354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {4E426B56-D589-44EA-B7A1-72ED5343B354}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {4E426B56-D589-44EA-B7A1-72ED5343B354}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {4E426B56-D589-44EA-B7A1-72ED5343B354}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {2013BD2B-0FBF-4CE3-AF48-A537C5836238}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/ApprovalBot/App_Start/WebApiConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Web.Http;
2 |
3 | namespace ApprovalBot
4 | {
5 | public static class WebApiConfig
6 | {
7 | public static void Register(HttpConfiguration config)
8 | {
9 | // Json settings
10 | //config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
11 | //config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
12 | //config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
13 | //JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
14 | //{
15 | // ContractResolver = new CamelCasePropertyNamesContractResolver(),
16 | // Formatting = Newtonsoft.Json.Formatting.Indented,
17 | // NullValueHandling = NullValueHandling.Ignore,
18 | //};
19 |
20 | // Web API configuration and services
21 |
22 | // Web API routes
23 | config.MapHttpAttributeRoutes();
24 |
25 | config.Routes.MapHttpRoute(
26 | name: "DefaultApi",
27 | routeTemplate: "api/{controller}/{id}",
28 | defaults: new { id = RouteParameter.Optional }
29 | );
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ApprovalBot/ApprovalBot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 |
8 |
9 | 2.0
10 | {4E426B56-D589-44EA-B7A1-72ED5343B354}
11 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
12 | Library
13 | Properties
14 | ApprovalBot
15 | Bot Application
16 | v4.6
17 | true
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | true
30 | full
31 | false
32 | bin\
33 | DEBUG;TRACE
34 | prompt
35 | 4
36 |
37 |
38 | pdbonly
39 | true
40 | bin\
41 | TRACE
42 | prompt
43 | 4
44 |
45 |
46 |
47 | ..\packages\AdaptiveCards.1.0.0\lib\net452\AdaptiveCards.dll
48 |
49 |
50 | ..\packages\Autofac.4.5.0\lib\net45\Autofac.dll
51 |
52 |
53 | $(SolutionDir)\packages\Chronic.Signed.0.3.2\lib\net40\Chronic.dll
54 |
55 |
56 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
57 |
58 |
59 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
60 |
61 |
62 | ..\packages\Microsoft.Azure.DocumentDB.1.22.0\lib\net45\Microsoft.Azure.Documents.Client.dll
63 |
64 |
65 | ..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll
66 |
67 |
68 | ..\packages\Microsoft.Bot.Builder.3.20.1\lib\net46\Microsoft.Bot.Builder.dll
69 |
70 |
71 | ..\packages\Microsoft.Bot.Builder.3.20.1\lib\net46\Microsoft.Bot.Builder.Autofac.dll
72 |
73 |
74 | ..\packages\Microsoft.Bot.Builder.Azure.3.16.3.40383\lib\net46\Microsoft.Bot.Builder.Azure.dll
75 |
76 |
77 | ..\packages\Microsoft.Bot.Builder.History.3.16.1.38846\lib\net46\Microsoft.Bot.Builder.History.dll
78 |
79 |
80 | ..\packages\Microsoft.Bot.Connector.3.20.1\lib\net46\Microsoft.Bot.Connector.dll
81 |
82 |
83 |
84 | ..\packages\Microsoft.Data.Edm.5.7.0\lib\net40\Microsoft.Data.Edm.dll
85 |
86 |
87 | ..\packages\Microsoft.Data.OData.5.7.0\lib\net40\Microsoft.Data.OData.dll
88 |
89 |
90 | ..\packages\Microsoft.Data.Services.Client.5.7.0\lib\net40\Microsoft.Data.Services.Client.dll
91 |
92 |
93 | ..\packages\Microsoft.Graph.1.12.0\lib\net45\Microsoft.Graph.dll
94 |
95 |
96 | ..\packages\Microsoft.Graph.Core.1.12.0\lib\net45\Microsoft.Graph.Core.dll
97 |
98 |
99 | ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.4.4.0\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll
100 |
101 |
102 | ..\packages\Microsoft.IdentityModel.Logging.5.2.1\lib\net451\Microsoft.IdentityModel.Logging.dll
103 |
104 |
105 | $(SolutionDir)\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.4.403061554\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll
106 |
107 |
108 | ..\packages\Microsoft.IdentityModel.Protocols.5.2.1\lib\net451\Microsoft.IdentityModel.Protocols.dll
109 |
110 |
111 | ..\packages\Microsoft.IdentityModel.Protocols.OpenIdConnect.5.2.1\lib\net451\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
112 |
113 |
114 | ..\packages\Microsoft.IdentityModel.Tokens.5.2.1\lib\net451\Microsoft.IdentityModel.Tokens.dll
115 |
116 |
117 | ..\packages\Microsoft.O365.ActionableMessages.Utilities.2.0.1\lib\net452\Microsoft.O365.ActionableMessages.Utilities.dll
118 |
119 |
120 | ..\packages\Microsoft.Owin.4.0.0\lib\net451\Microsoft.Owin.dll
121 |
122 |
123 | ..\packages\Microsoft.Owin.Security.4.0.0\lib\net451\Microsoft.Owin.Security.dll
124 |
125 |
126 | ..\packages\Microsoft.Owin.Security.OpenIdConnect.4.0.0\lib\net451\Microsoft.Owin.Security.OpenIdConnect.dll
127 |
128 |
129 | ..\packages\Microsoft.Rest.ClientRuntime.2.3.8\lib\net452\Microsoft.Rest.ClientRuntime.dll
130 |
131 |
132 | $(SolutionDir)\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll
133 |
134 |
135 | ..\packages\WindowsAzure.Storage.7.2.1\lib\net40\Microsoft.WindowsAzure.Storage.dll
136 |
137 |
138 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
139 |
140 |
141 | ..\packages\Owin.1.0\lib\net40\Owin.dll
142 |
143 |
144 |
145 |
146 | ..\packages\System.IdentityModel.Tokens.Jwt.5.2.1\lib\net451\System.IdentityModel.Tokens.Jwt.dll
147 |
148 |
149 |
150 | ..\packages\System.Net.Http.4.3.1\lib\net46\System.Net.Http.dll
151 |
152 |
153 | $(SolutionDir)\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll
154 |
155 |
156 |
157 |
158 | ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll
159 | True
160 |
161 |
162 | ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll
163 |
164 |
165 | ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll
166 |
167 |
168 | ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll
169 | True
170 |
171 |
172 | ..\packages\System.Spatial.5.7.0\lib\net40\System.Spatial.dll
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | $(SolutionDir)\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll
185 |
186 |
187 | $(SolutionDir)\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | Designer
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | Global.asax
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 | Designer
231 |
232 |
233 | Designer
234 |
235 |
236 |
237 |
238 | Web.config
239 |
240 |
241 | Web.config
242 |
243 |
244 |
245 | 10.0
246 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
247 |
248 |
249 | true
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 | True
259 | True
260 | 3979
261 | /
262 | http://localhost:3979/
263 | False
264 | False
265 |
266 |
267 | False
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
276 |
277 |
278 |
279 |
286 |
--------------------------------------------------------------------------------
/ApprovalBot/Controllers/MessagesController.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using System.Web.Http;
5 | using Microsoft.Bot.Builder.Dialogs;
6 | using Microsoft.Bot.Connector;
7 |
8 | namespace ApprovalBot
9 | {
10 | [BotAuthentication]
11 | public class MessagesController : ApiController
12 | {
13 | ///
14 | /// POST: api/Messages
15 | /// Receive a message from a user and reply to it
16 | ///
17 | public async Task Post([FromBody]Activity activity)
18 | {
19 | if (activity.Type == ActivityTypes.Message)
20 | {
21 | await Conversation.SendAsync(activity, () => new Dialogs.ExceptionHandlerDialog(new Dialogs.RootDialog()));
22 | }
23 | else
24 | {
25 | await HandleSystemMessage(activity);
26 | }
27 | var response = Request.CreateResponse(HttpStatusCode.OK);
28 | return response;
29 | }
30 |
31 | private async Task HandleSystemMessage(Activity message)
32 | {
33 | if (message.Type == ActivityTypes.DeleteUserData)
34 | {
35 | // Implement user deletion here
36 | // If we handle user deletion, return a real message
37 | }
38 | else if (message.Type == ActivityTypes.ConversationUpdate)
39 | {
40 | // Handle conversation state changes, like members being added and removed
41 | // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
42 | // Not available in all channels
43 | }
44 | else if (message.Type == ActivityTypes.ContactRelationUpdate)
45 | {
46 | // Handle add/remove from contact lists
47 | // Activity.From + Activity.Action represent what happened
48 | }
49 | else if (message.Type == ActivityTypes.Typing)
50 | {
51 | // Handle knowing that the user is typing
52 | }
53 | else if (message.Type == ActivityTypes.Event)
54 | {
55 | // Send TokenResponse Events along to the Dialog stack
56 | if (message.IsTokenResponseEvent())
57 | {
58 | await Conversation.SendAsync(message, () => new Dialogs.RootDialog());
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/ApprovalBot/Controllers/ResponsesController.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using ApprovalBot.Helpers;
3 | using ApprovalBot.Models;
4 | using Microsoft.O365.ActionableMessages.Utilities;
5 | using System;
6 | using System.Configuration;
7 | using System.Net;
8 | using System.Net.Http;
9 | using System.Net.Http.Headers;
10 | using System.Threading.Tasks;
11 | using System.Web.Http;
12 |
13 | namespace ApprovalBot.Controllers
14 | {
15 | public class ResponsesController : ApiController
16 | {
17 | private readonly string baseUrl = ConfigurationManager.AppSettings["AppRootUrl"];
18 | private readonly string amSender = ConfigurationManager.AppSettings["SenderEmail"];
19 | private readonly string ngrokUrl = ConfigurationManager.AppSettings["NgrokRootUrl"];
20 | public async Task PostResponse(ActionableEmailResponse response)
21 | {
22 | // Validate the authorization header
23 | bool isTokenValid = await ValidateAuthorizationHeader(Request.Headers.Authorization,
24 | string.IsNullOrEmpty(ngrokUrl) ? baseUrl : ngrokUrl, response.UserEmail);
25 | if (!isTokenValid)
26 | {
27 | return Unauthorized();
28 | }
29 |
30 | // Get the approval request
31 | var approval = await DatabaseHelper.GetApprovalAsync(response.ApprovalId);
32 | if (approval == null)
33 | {
34 | return BadRequest("Invalid approval ID");
35 | }
36 |
37 | // Find the user in approvers
38 | bool userIsAnApprover = false;
39 | foreach(var approver in approval.Approvers)
40 | {
41 | if (approver.EmailAddress == response.UserEmail)
42 | {
43 | userIsAnApprover = true;
44 | approver.Response = response.Response == "approved" ? ResponseStatus.Approved : ResponseStatus.Rejected;
45 | approver.ResponseNote = response.Notes;
46 | }
47 | }
48 |
49 | if (userIsAnApprover)
50 | {
51 | // Update database
52 | await DatabaseHelper.UpdateApprovalAsync(approval.Id, approval);
53 | return GenerateResponseCard(approval);
54 | }
55 |
56 | return BadRequest("User is not an approver");
57 | }
58 |
59 | private IHttpActionResult GenerateResponseCard(Approval approval)
60 | {
61 | var responseCard = ApprovalStatusHelper.GetApprovalStatusCard(approval);
62 |
63 | // Modify card for this use case
64 | responseCard.Body.Insert(0, new AdaptiveTextBlock()
65 | {
66 | Text = $"Response status as of {TimeZoneHelper.GetAdaptiveDateTimeString(DateTimeOffset.UtcNow)}:"
67 | });
68 |
69 | responseCard.Body.Insert(0, new AdaptiveTextBlock()
70 | {
71 | Text = "Thanks for responding. Your response has been recorded."
72 | });
73 |
74 | HttpResponseMessage refreshCardResponse = new HttpResponseMessage(HttpStatusCode.OK);
75 | refreshCardResponse.Headers.Add("CARD-UPDATE-IN-BODY", "true");
76 |
77 | refreshCardResponse.Content = new StringContent(responseCard.ToJson(), System.Text.Encoding.UTF8, "application/json");
78 | return ResponseMessage(refreshCardResponse);
79 | }
80 |
81 | private async Task ValidateAuthorizationHeader(AuthenticationHeaderValue authHeader, string targetUrl, string userId)
82 | {
83 | // Validate that we have a bearer token
84 | if (authHeader == null ||
85 | !string.Equals(authHeader.Scheme, "bearer", StringComparison.OrdinalIgnoreCase) ||
86 | string.IsNullOrEmpty(authHeader.Parameter))
87 | {
88 | return false;
89 | }
90 |
91 | // Validate the token
92 | ActionableMessageTokenValidator validator = new ActionableMessageTokenValidator();
93 | ActionableMessageTokenValidationResult result = await validator.ValidateTokenAsync(authHeader.Parameter, targetUrl);
94 | if (!result.ValidationSucceeded)
95 | {
96 | return false;
97 | }
98 |
99 | // Token is valid, now check the sender and action performer
100 | // Both should equal the user
101 | if (!string.Equals(result.ActionPerformer, userId, StringComparison.OrdinalIgnoreCase) ||
102 | !string.Equals(result.Sender, string.IsNullOrEmpty(amSender) ? userId : amSender, StringComparison.OrdinalIgnoreCase))
103 | {
104 | return false;
105 | }
106 |
107 | return true;
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/ApprovalBot/Dialogs/ExceptionHandlerDialog.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Bot.Builder.Dialogs;
2 | using System;
3 | using System.Configuration;
4 | using System.Threading.Tasks;
5 |
6 | namespace ApprovalBot.Dialogs
7 | {
8 | [Serializable]
9 | public class ExceptionHandlerDialog : IDialog
10 | {
11 | private readonly IDialog dialog;
12 | private readonly bool displayException =
13 | string.IsNullOrEmpty(ConfigurationManager.AppSettings["DisplayExceptions"]) ? false :
14 | Convert.ToBoolean(ConfigurationManager.AppSettings["DisplayExceptions"]);
15 |
16 | public ExceptionHandlerDialog(IDialog dialog)
17 | {
18 | this.dialog = dialog;
19 | }
20 |
21 | public async Task StartAsync(IDialogContext context)
22 | {
23 | try
24 | {
25 | context.Call(dialog, ResumeAsync);
26 | }
27 | catch (Exception ex)
28 | {
29 | await LogException(ex);
30 |
31 | if (displayException)
32 | {
33 | await DisplayException(context, ex).ConfigureAwait(false);
34 | }
35 | }
36 | }
37 |
38 | public async Task ResumeAsync(IDialogContext context, IAwaitable result)
39 | {
40 | try
41 | {
42 | context.Done(await result);
43 | }
44 | catch (Exception ex)
45 | {
46 | await LogException(ex);
47 |
48 | if (displayException)
49 | {
50 | await DisplayException(context, ex).ConfigureAwait(false);
51 | }
52 | }
53 | }
54 |
55 | private async Task DisplayException(IDialogContext context, Exception ex)
56 | {
57 | var message = ex.Message.Replace(Environment.NewLine, "** \n**");
58 | var stack = ex.StackTrace.Replace(Environment.NewLine, " \n");
59 |
60 | await context.PostAsync($"**{message}** \n\n{stack}").ConfigureAwait(false);
61 | }
62 |
63 | private async Task LogException(Exception ex)
64 | {
65 | var exceptionLog = new Helpers.GraphLogEntry(ex, string.Empty);
66 | await Helpers.DatabaseHelper.AddGraphLog(exceptionLog);
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/ApprovalBot/Dialogs/RootDialog.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using ApprovalBot.Helpers;
3 | using ApprovalBot.Models;
4 | using Microsoft.Bot.Builder.Dialogs;
5 | using Microsoft.Bot.Connector;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Configuration;
9 | using System.Threading.Tasks;
10 |
11 | namespace ApprovalBot.Dialogs
12 | {
13 | [Serializable]
14 | public class RootDialog : IDialog
15 | {
16 | private static string ConnectionName = ConfigurationManager.AppSettings["ConnectionName"];
17 |
18 | public async Task StartAsync(IDialogContext context)
19 | {
20 | context.Wait(MessageReceivedAsync);
21 | }
22 |
23 | private async Task MessageReceivedAsync(IDialogContext context, IAwaitable result)
24 | {
25 | var activity = await result as Activity;
26 | string userText = string.IsNullOrEmpty(activity.Text) ? string.Empty : activity.Text.ToLower();
27 |
28 | // Handle the text as appropriate
29 | if (IsMessageEmpty(activity) || userText.StartsWith("help"))
30 | {
31 | await ShowHelp(context);
32 | context.Wait(MessageReceivedAsync);
33 | }
34 |
35 | else if (userText.StartsWith("hi") || userText.StartsWith("hello") || userText.StartsWith("hey"))
36 | {
37 | await context.PostAsync(@"Hi! What can I do for you? (try ""get approval"" to start an approval request)");
38 | context.Wait(MessageReceivedAsync);
39 | }
40 |
41 | else if (userText.StartsWith("logout"))
42 | {
43 | await context.SignOutUserAsync(ConnectionName);
44 | await context.PostAsync("You are now logged out.");
45 | context.Wait(MessageReceivedAsync);
46 | }
47 |
48 | else if (userText.StartsWith("reset demo please"))
49 | {
50 | await ResetDemo(context, activity);
51 | await context.PostAsync("DEMO RESET");
52 | context.Wait(MessageReceivedAsync);
53 | }
54 |
55 | // Anything else requires auth
56 | // The text can be empty when receiving a message back from a card
57 | // button.
58 | else if (string.IsNullOrEmpty(userText))
59 | {
60 | // Handle card action data
61 | var actionData = ActionData.Parse(activity.Value);
62 |
63 | // When the user selects a file in the select file card
64 | if (actionData.CardAction == CardActionTypes.SelectFile)
65 | {
66 | // Save selected file to conversation state
67 | context.ConversationData.SetValue("selectedFile", actionData.SelectedFile);
68 | // Show file detail card and confirm selection
69 | context.Call(CreateGetTokenDialog(), ConfirmFile);
70 | //reply = await ConfirmFile(context, activity, accessToken.Token, actionData.SelectedFile);
71 | }
72 | // When the user clicks the "send approval request" button
73 | else if (actionData.CardAction == CardActionTypes.SendApprovalRequest)
74 | {
75 | // Check approvers
76 | if (string.IsNullOrEmpty(actionData.Approvers))
77 | {
78 | SaveMissingInfoState(context, "approvers", actionData);
79 | await context.PostAsync("I need at least one approver email address to send to. Who should I send to?");
80 | context.Wait(MessageReceivedAsync);
81 | }
82 |
83 | else
84 | {
85 | string[] approvers = EmailHelper.ConvertDelimitedAddressStringToArray(actionData.Approvers.Trim());
86 | if (approvers == null)
87 | {
88 | SaveMissingInfoState(context, "approvers", actionData);
89 | await context.PostAsync($"One or more values in **{actionData.Approvers}** is not a valid SMTP email address. Can you give me the list of approvers again?");
90 | context.Wait(MessageReceivedAsync);
91 | }
92 | else
93 | {
94 | //await ShowTyping(context, activity);
95 | // Save user ID, selected file, and approvers to conversation state
96 | context.ConversationData.SetValue("approvalRequestor", activity.From.Id);
97 | context.ConversationData.SetValue("selectedFile", actionData.SelectedFile);
98 | context.ConversationData.SetValue("approvers", approvers);
99 |
100 | context.Call(CreateGetTokenDialog(), SendApprovalRequest);
101 | }
102 | }
103 | }
104 | // User clicked the "no" button when confirming the file.
105 | else if (actionData.CardAction == CardActionTypes.WrongFile)
106 | {
107 | // Re-prompt for a file
108 | context.Call(CreateGetTokenDialog(), PromptForFile);
109 | }
110 | // User selected a pending approval to check status
111 | else if (actionData.CardAction == CardActionTypes.SelectApproval)
112 | {
113 | // Save the selected approval to conversation state
114 | context.ConversationData.SetValue("selectedApproval", actionData.SelectedApproval);
115 | context.Call(CreateGetTokenDialog(), GetApprovalStatus);
116 | }
117 | else
118 | {
119 | await context.PostAsync(@"I'm sorry, I don't understand what you want me to do. Type ""help"" to see a list of things I can do.");
120 | context.Wait(MessageReceivedAsync);
121 | }
122 | }
123 | else if (userText.StartsWith("get approval"))
124 | {
125 | RemoveMissingInfoState(context);
126 | context.Call(CreateGetTokenDialog(), PromptForFile);
127 | }
128 | else if (userText.StartsWith("check status"))
129 | {
130 | RemoveMissingInfoState(context);
131 | // Save user ID to conversation state
132 | context.ConversationData.SetValue("approvalRequestor", activity.From.Id);
133 | context.Call(CreateGetTokenDialog(), PromptForApprovalRequest);
134 | }
135 | else if (!string.IsNullOrEmpty(ExpectedMissingInfo(context)))
136 | {
137 | string missingField = ExpectedMissingInfo(context);
138 |
139 | if (missingField == "approvers")
140 | {
141 | // Validate input
142 | string[] approvers = EmailHelper.ConvertDelimitedAddressStringToArray(userText.Trim());
143 | if (approvers == null)
144 | {
145 | await context.PostAsync(@"Sorry, I'm still having trouble. Please enter the approvers again, keeping in mind:
146 | - Use full SMTP email addresses, like `bob@contsoso.com`
147 | - Separate multiple email addresses with a semicolon (`;`), like `bob@contoso.com;allie@contoso.com`");
148 | }
149 | else
150 | {
151 | ActionData actionData = context.ConversationData.GetValue("actionData");
152 | RemoveMissingInfoState(context);
153 | //await ShowTyping(context, activity);
154 |
155 | // Save user ID, selected file, and approvers to conversation state
156 | context.ConversationData.SetValue("approvalRequestor", activity.From.Id);
157 | context.ConversationData.SetValue("selectedFile", actionData.SelectedFile);
158 | context.ConversationData.SetValue("approvers", approvers);
159 |
160 | context.Call(CreateGetTokenDialog(), SendApprovalRequest);
161 | }
162 | }
163 | }
164 | else
165 | {
166 | await context.PostAsync(@"I'm sorry, I don't understand what you want me to do. Type ""help"" to see a list of things I can do.");
167 | context.Wait(MessageReceivedAsync);
168 | }
169 | }
170 |
171 | private bool IsMessageEmpty(Activity activity)
172 | {
173 | return string.IsNullOrWhiteSpace(activity.Text) && activity.Value == null;
174 | }
175 |
176 | private void SaveMissingInfoState(IDialogContext context, string field, ActionData actionData)
177 | {
178 | context.ConversationData.SetValue("missingField", field);
179 | context.ConversationData.SetValue("actionData", actionData);
180 | }
181 |
182 | private string ExpectedMissingInfo(IDialogContext context)
183 | {
184 | return context.ConversationData.GetValueOrDefault("missingField");
185 | }
186 |
187 | private void RemoveMissingInfoState(IDialogContext context)
188 | {
189 | context.ConversationData.RemoveValue("missingField");
190 | context.ConversationData.RemoveValue("actionData");
191 | }
192 |
193 | private void ClearConversationData(IDialogContext context, params object[] values)
194 | {
195 | foreach (string value in values)
196 | {
197 | context.ConversationData.RemoveValue(value);
198 | }
199 | }
200 |
201 | private async Task PromptForFile(IDialogContext context, IAwaitable tokenResponse)
202 | {
203 | //await ShowTyping(context, activity);
204 | var accessToken = await tokenResponse;
205 |
206 | if (accessToken == null || string.IsNullOrEmpty(accessToken.Token))
207 | {
208 | await context.PostAsync(Apologize("I could not get an access token"));
209 | return;
210 | }
211 |
212 | // Get a list of files to choose from
213 | AdaptiveCard pickerCard = null;
214 |
215 | try
216 | {
217 | pickerCard = await GraphHelper.GetFilePickerCardFromOneDrive(accessToken.Token);
218 | }
219 | catch (Microsoft.Graph.ServiceException ex)
220 | {
221 | if (ex.Error.Code == "UnknownError" && ex.Message.Contains("Invalid Hostname"))
222 | {
223 | // Log that this happened
224 | await DatabaseHelper.AddGraphLog(new GraphLogEntry(ex, "retry-get-file-picker"));
225 | // retry
226 | pickerCard = await GraphHelper.GetFilePickerCardFromOneDrive(accessToken.Token);
227 | }
228 | else
229 | {
230 | throw;
231 | }
232 | }
233 |
234 | if (pickerCard != null)
235 | {
236 | var reply = context.MakeMessage();
237 | reply.Attachments = new List()
238 | {
239 | new Attachment() { ContentType = AdaptiveCard.ContentType, Content = pickerCard }
240 | };
241 |
242 | await context.PostAsync(reply);
243 | }
244 | else
245 | {
246 | await context.PostAsync("I couldn't find any files in your OneDrive. Please add the file you want approved and try again.");
247 | }
248 | }
249 |
250 | private async Task ConfirmFile(IDialogContext context, IAwaitable tokenResponse)
251 | {
252 | var accessToken = await tokenResponse;
253 | var fileId = context.ConversationData.GetValueOrDefault("selectedFile");
254 |
255 | ClearConversationData(context, "selectedFile");
256 |
257 | if (accessToken == null || string.IsNullOrEmpty(accessToken.Token))
258 | {
259 | await context.PostAsync(Apologize("I could not get an access token"));
260 | return;
261 | }
262 |
263 | if (string.IsNullOrEmpty(fileId))
264 | {
265 | await context.PostAsync(Apologize("I could not find a selected file ID in the conversation state"));
266 | return;
267 | }
268 |
269 | //await ShowTyping(context, activity);
270 | var reply = context.MakeMessage();
271 | AdaptiveCard fileDetailCard = null;
272 |
273 | try
274 | {
275 | fileDetailCard = await GraphHelper.GetFileDetailCard(accessToken.Token, fileId);
276 | }
277 | catch (Microsoft.Graph.ServiceException ex)
278 | {
279 | if (ex.Error.Code == "UnknownError" && ex.Message.Contains("Invalid Hostname"))
280 | {
281 | // Log that this happened
282 | await DatabaseHelper.AddGraphLog(new GraphLogEntry(ex, "retry-get-file-detail"));
283 | // retry
284 | fileDetailCard = await GraphHelper.GetFileDetailCard(accessToken.Token, fileId);
285 | }
286 | else
287 | {
288 | throw;
289 | }
290 | }
291 |
292 | reply.Attachments = new List()
293 | {
294 | new Attachment() { ContentType = AdaptiveCard.ContentType, Content = fileDetailCard }
295 | };
296 |
297 | await context.PostAsync(reply);
298 | }
299 |
300 | private async Task PromptForApprovalRequest(IDialogContext context, IAwaitable tokenResponse)
301 | {
302 | //await ShowTyping(context, activity);
303 | var accessToken = await tokenResponse;
304 | var userId = context.ConversationData.GetValueOrDefault("approvalRequestor");
305 |
306 | ClearConversationData(context, "approvalRequestor");
307 |
308 | if (accessToken == null || string.IsNullOrEmpty(accessToken.Token))
309 | {
310 | await context.PostAsync(Apologize("I could not get an access token"));
311 | return;
312 | }
313 |
314 | if (string.IsNullOrEmpty(userId))
315 | {
316 | await context.PostAsync(Apologize("I could not find a user ID in the conversation state"));
317 | return;
318 | }
319 |
320 | var statusCardList = await ApprovalStatusHelper.GetApprovalsForUserCard(accessToken.Token, userId);
321 | if (statusCardList == null)
322 | {
323 | await context.PostAsync("I'm sorry, but I didn't find any approvals requested by you.");
324 | }
325 | else
326 | {
327 | var reply = context.MakeMessage();
328 |
329 | if (statusCardList.Count > 1)
330 | {
331 | reply.Text = "Select an approval to see its status.";
332 | reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;
333 | }
334 |
335 | reply.Attachments = new List();
336 |
337 | foreach(var card in statusCardList)
338 | {
339 | reply.Attachments.Add(new Attachment()
340 | {
341 | ContentType = AdaptiveCard.ContentType,
342 | Content = card
343 | });
344 | }
345 |
346 | await context.PostAsync(reply);
347 | }
348 | }
349 |
350 | private async Task SendApprovalRequest(IDialogContext context, IAwaitable tokenResponse)
351 | {
352 | var accessToken = await tokenResponse;
353 | var userId = context.ConversationData.GetValueOrDefault("approvalRequestor");
354 | var fileId = context.ConversationData.GetValueOrDefault("selectedFile");
355 | var approvers = context.ConversationData.GetValueOrDefault("approvers");
356 |
357 | ClearConversationData(context, "approvalRequestor", "selectedFile", "approvers");
358 |
359 | if (accessToken == null || string.IsNullOrEmpty(accessToken.Token))
360 | {
361 | await context.PostAsync(Apologize("I could not get an access token"));
362 | return;
363 | }
364 |
365 | if (string.IsNullOrEmpty(userId))
366 | {
367 | await context.PostAsync(Apologize("I could not find a user ID in the conversation state"));
368 | return;
369 | }
370 |
371 | if (string.IsNullOrEmpty(fileId))
372 | {
373 | await context.PostAsync(Apologize("I could not find a selected file ID in the conversation state"));
374 | return;
375 | }
376 |
377 | if (string.IsNullOrEmpty(fileId))
378 | {
379 | await context.PostAsync(Apologize("I could not find list of approvers in the conversation state"));
380 | return;
381 | }
382 |
383 | try
384 | {
385 | await ApprovalRequestHelper.SendApprovalRequest(accessToken.Token, userId, fileId, approvers);
386 | }
387 | catch (Microsoft.Graph.ServiceException ex)
388 | {
389 | if (ex.Error.Code == "UnknownError" && ex.Message.Contains("Invalid Hostname"))
390 | {
391 | // Log that this happened
392 | await DatabaseHelper.AddGraphLog(new GraphLogEntry(ex, "retry-send-approval"));
393 | // retry
394 | await ApprovalRequestHelper.SendApprovalRequest(accessToken.Token, userId, fileId, approvers);
395 | }
396 | else
397 | {
398 | throw;
399 | }
400 | }
401 |
402 | await context.PostAsync(@"I've sent the request. You can check the status of your request by typing ""check status"".");
403 | }
404 |
405 | private async Task GetApprovalStatus(IDialogContext context, IAwaitable tokenResponse)
406 | {
407 | //await ShowTyping(context, activity);
408 | var approvalId = context.ConversationData.GetValueOrDefault("selectedApproval");
409 | ClearConversationData(context, "selectedApproval");
410 |
411 | if (string.IsNullOrEmpty(approvalId))
412 | {
413 | await context.PostAsync(Apologize("I could not find a selected approval ID in the conversation state"));
414 | return;
415 | }
416 |
417 | var statusCard = await ApprovalStatusHelper.GetApprovalStatusCard(approvalId);
418 |
419 | await context.PostAsync("Here's what I found:");
420 |
421 | var reply = context.MakeMessage();
422 | reply.Attachments = new List()
423 | {
424 | new Attachment() { ContentType = AdaptiveCard.ContentType, Content = statusCard }
425 | };
426 |
427 | await context.PostAsync(reply);
428 | }
429 |
430 | private async Task ShowTyping(IDialogContext context, Activity activity)
431 | {
432 | var typing = activity.CreateReply();
433 | typing.Type = ActivityTypes.Typing;
434 | await context.PostAsync(typing);
435 | }
436 |
437 | private async Task ShowHelp(IDialogContext context)
438 | {
439 | await context.PostAsync("I am ApprovalBot. I can help you get public release approval for any documents in your OneDrive for Business.");
440 | await context.PostAsync(@"Try one of the following commands to get started:
441 |
442 | - ""get approval""
443 | - ""check status""");
444 | }
445 |
446 | private async Task ResetDemo(IDialogContext context, Activity activity)
447 | {
448 | // Remove any pending approvals
449 | await DatabaseHelper.DeleteAllUserApprovals(activity.From.Id);
450 |
451 | // Sign out
452 | await context.SignOutUserAsync(ConnectionName);
453 |
454 | context.UserData.Clear();
455 | }
456 |
457 | private GetTokenDialog CreateGetTokenDialog()
458 | {
459 | return new GetTokenDialog(
460 | ConnectionName,
461 | $"Please sign in to ApprovalBot to proceed.",
462 | "Sign In",
463 | 2,
464 | "Hmm. Something went wrong, let's try again.");
465 | }
466 |
467 | private string Apologize(string moreInfo)
468 | {
469 | return $"I'm sorry, I seem to have gotten my circuits crossed. Please tell a human that ${moreInfo}.";
470 | }
471 | }
472 | }
--------------------------------------------------------------------------------
/ApprovalBot/Global.asax:
--------------------------------------------------------------------------------
1 | <%@ Application Codebehind="Global.asax.cs" Inherits="ApprovalBot.WebApiApplication" Language="C#" %>
2 |
--------------------------------------------------------------------------------
/ApprovalBot/Global.asax.cs:
--------------------------------------------------------------------------------
1 | using ApprovalBot.Helpers;
2 | using Autofac;
3 | using Microsoft.Bot.Builder.Azure;
4 | using Microsoft.Bot.Builder.Dialogs;
5 | using Microsoft.Bot.Builder.Dialogs.Internals;
6 | using Microsoft.Bot.Connector;
7 | using System;
8 | using System.Configuration;
9 | using System.Reflection;
10 | using System.Web.Http;
11 |
12 | namespace ApprovalBot
13 | {
14 | public class WebApiApplication : System.Web.HttpApplication
15 | {
16 | protected void Application_Start()
17 | {
18 | GlobalConfiguration.Configure(WebApiConfig.Register);
19 |
20 | Conversation.UpdateContainer(
21 | builder =>
22 | {
23 | builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
24 |
25 | // This will create a CosmosDB store, suitable for production
26 | // NOTE: Requires an actual CosmosDB instance and configuration in
27 | // PrivateSettings.config
28 | var databaseUri = new Uri(ConfigurationManager.AppSettings["DatabaseUri"]);
29 | var databaseKey = ConfigurationManager.AppSettings["DatabaseKey"];
30 | var store = new DocumentDbBotDataStore(databaseUri, databaseKey);
31 |
32 | builder.Register(c => store)
33 | .Keyed>(AzureModule.Key_DataStore)
34 | .AsSelf()
35 | .SingleInstance();
36 | });
37 |
38 | // Initialize approvals database
39 | DatabaseHelper.Initialize();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/ApprovalRequestHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Threading.Tasks;
5 | using AdaptiveCards;
6 | using ApprovalBot.Models;
7 | using ApprovalBot.OutlookAdaptive;
8 | using Microsoft.Graph;
9 |
10 | namespace ApprovalBot.Helpers
11 | {
12 | public static class ApprovalRequestHelper
13 | {
14 | private static readonly string originatorId = ConfigurationManager.AppSettings["OriginatorId"];
15 | private static readonly string messageSender = ConfigurationManager.AppSettings["SenderEmail"];
16 | private static readonly string actionBaseUrl = ConfigurationManager.AppSettings["AppRootUrl"];
17 | private static readonly string ngrokUrl = ConfigurationManager.AppSettings["NgrokRootUrl"];
18 |
19 | public static async Task SendApprovalRequest(string accessToken, string userId, string fileId, string[] approvers)
20 | {
21 | // Get file info
22 | var fileInfo = await GraphHelper.GetFileInfo(accessToken, fileId);
23 |
24 | // Info on requestor
25 | var requestor = await GraphHelper.GetUser(accessToken);
26 |
27 | // Requestor pic URI
28 | var requestorPicUri = await GraphHelper.GetUserPhotoDataUri(accessToken, "48x48");
29 |
30 | // File thumbnail URI
31 | var thumbnailUri = await GraphHelper.GetFileThumbnailDataUri(accessToken, fileInfo.Id);
32 |
33 | // Create an approval to store in the database
34 | var approval = new Approval()
35 | {
36 | Requestor = userId,
37 | File = fileInfo,
38 | Approvers = new List(),
39 | RequestDate = DateTimeOffset.UtcNow
40 | };
41 |
42 | // Add approvers
43 | foreach (string approver in approvers)
44 | {
45 | approval.Approvers.Add(new ApproverInfo()
46 | {
47 | EmailAddress = approver,
48 | Response = Models.ResponseStatus.NotResponded,
49 | ResponseNote = string.Empty
50 | });
51 | }
52 |
53 | // Add to database
54 | var dbApproval = await DatabaseHelper.CreateApprovalAsync(approval);
55 |
56 | // Build and send card for each recipient
57 | foreach (string approver in approvers)
58 | {
59 | var approvalRequestCard = BuildRequestCard(requestor, requestorPicUri,
60 | fileInfo, thumbnailUri, approver, dbApproval.Id);
61 |
62 | await GraphHelper.SendRequestCard(accessToken, approvalRequestCard, approver, messageSender);
63 | }
64 | }
65 |
66 | private static AdaptiveCard BuildRequestCard(User requestor, string requestorPicUri, ApprovalFileInfo fileInfo, string thumbnailUri, string recipient, string approvalId)
67 | {
68 | // Build actionable email card
69 | var approvalRequestCard = new AdaptiveCard();
70 |
71 | // Outlook-specific property on AdaptiveCard
72 | if (!string.IsNullOrEmpty(originatorId))
73 | {
74 | approvalRequestCard.AdditionalProperties.Add("originator", originatorId);
75 | }
76 |
77 | approvalRequestCard.Body.Add(new AdaptiveTextBlock()
78 | {
79 | Text = "Pending Approval",
80 | Weight = AdaptiveTextWeight.Bolder,
81 | Size = AdaptiveTextSize.Large
82 | });
83 |
84 | approvalRequestCard.Body.Add(new AdaptiveTextBlock()
85 | {
86 | Text = "Requested by"
87 | });
88 |
89 |
90 | var requestorColumnSet = new AdaptiveColumnSet();
91 |
92 | if (!string.IsNullOrEmpty(requestorPicUri))
93 | {
94 | var requestorPic = new AdaptiveImage()
95 | {
96 | Style = AdaptiveImageStyle.Person,
97 | Url = new Uri(requestorPicUri)
98 | };
99 |
100 | // Outlook-specific property on Image
101 | requestorPic.AdditionalProperties.Add("pixelWidth", "48");
102 |
103 | var requestorPicCol = new AdaptiveColumn()
104 | {
105 | Width = AdaptiveColumnWidth.Auto.ToLower()
106 | };
107 |
108 | requestorPicCol.Items.Add(requestorPic);
109 |
110 | requestorColumnSet.Columns.Add(requestorPicCol);
111 | }
112 |
113 | var requestorNameCol = new AdaptiveColumn()
114 | {
115 | Width = AdaptiveColumnWidth.Stretch.ToLower()
116 | };
117 |
118 | // Outlook-specific property on Column
119 | requestorNameCol.AdditionalProperties.Add("verticalContentAlignment", "center");
120 |
121 | requestorNameCol.Items.Add(new AdaptiveTextBlock()
122 | {
123 | Size = AdaptiveTextSize.Medium,
124 | Text = requestor.DisplayName
125 | });
126 |
127 | requestorNameCol.Items.Add(new AdaptiveTextBlock()
128 | {
129 | IsSubtle = true,
130 | Spacing = AdaptiveSpacing.None,
131 | Text = requestor.Mail
132 | });
133 |
134 | requestorColumnSet.Columns.Add(requestorNameCol);
135 |
136 | approvalRequestCard.Body.Add(requestorColumnSet);
137 |
138 | // File info
139 | approvalRequestCard.Body.Add(new AdaptiveTextBlock()
140 | {
141 | Text = "File needing approval",
142 | Separator = true
143 | });
144 |
145 | var fileColumnSet = new AdaptiveColumnSet()
146 | {
147 | SelectAction = new AdaptiveOpenUrlAction()
148 | {
149 | Title = fileInfo.Name,
150 | Url = new Uri(fileInfo.SharingUrl)
151 | }
152 | };
153 |
154 | if (!string.IsNullOrEmpty(thumbnailUri))
155 | {
156 | var fileThumb = new AdaptiveImage()
157 | {
158 | Url = new Uri(thumbnailUri)
159 | };
160 |
161 | // Outlook-specific property on Image
162 | fileThumb.AdditionalProperties.Add("pixelWidth", "48");
163 |
164 | var fileThumbCol = new AdaptiveColumn()
165 | {
166 | Width = AdaptiveColumnWidth.Auto.ToLower()
167 | };
168 |
169 | fileThumbCol.Items.Add(fileThumb);
170 |
171 | fileColumnSet.Columns.Add(fileThumbCol);
172 | }
173 |
174 | var fileNameCol = new AdaptiveColumn()
175 | {
176 | Width = AdaptiveColumnWidth.Stretch.ToLower()
177 | };
178 |
179 | // Outlook-specific property on Column
180 | fileNameCol.AdditionalProperties.Add("verticalContentAlignment", "center");
181 |
182 | fileNameCol.Items.Add(new AdaptiveTextBlock()
183 | {
184 | Size = AdaptiveTextSize.Medium,
185 | Text = fileInfo.Name
186 | });
187 |
188 | fileNameCol.Items.Add(new AdaptiveTextBlock()
189 | {
190 | IsSubtle = true,
191 | Spacing = AdaptiveSpacing.None,
192 | Text = "(tap to view)"
193 | });
194 |
195 | fileColumnSet.Columns.Add(fileNameCol);
196 |
197 | approvalRequestCard.Body.Add(fileColumnSet);
198 |
199 | // Respond button
200 |
201 | // Response form (hiddden initially)
202 | var responseForm = new AdaptiveCard();
203 | responseForm.AdditionalProperties.Add("style", "emphasis");
204 |
205 | responseForm.Body.Add(new AdaptiveTextInput()
206 | {
207 | Id = "notes",
208 | IsMultiline = true,
209 | Placeholder = "Enter any notes for the requestor"
210 | });
211 |
212 | responseForm.Actions.Add(new AdaptiveHttpAction()
213 | {
214 | Title = "Approve",
215 | Method = AdaptiveHttpActionMethod.POST,
216 | Url = new Uri($"{(string.IsNullOrEmpty(ngrokUrl) ? actionBaseUrl : ngrokUrl) }/api/responses"),
217 | Body = $@"{{ ""userEmail"": ""{recipient}"", ""approvalId"": ""{approvalId}"", ""response"": ""approved"", ""notes"": ""{{{{notes.value}}}}"" }}s"
218 | });
219 |
220 | responseForm.Actions.Add(new AdaptiveHttpAction()
221 | {
222 | Title = "Reject",
223 | Method = AdaptiveHttpActionMethod.POST,
224 | Url = new Uri($"{(string.IsNullOrEmpty(ngrokUrl) ? actionBaseUrl : ngrokUrl)}/api/responses"),
225 | Body = $@"{{ ""userEmail"": ""{recipient}"", ""approvalId"": ""{approvalId}"", ""response"": ""rejected"", ""notes"": ""{{{{notes.value}}}}"" }}"
226 | });
227 |
228 | approvalRequestCard.Actions.Add(new AdaptiveShowCardAction() {
229 | Title = "Respond",
230 | Card = responseForm
231 | });
232 |
233 | return approvalRequestCard;
234 | }
235 | }
236 | }
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/ApprovalStatusHelper.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using ApprovalBot.Models;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace ApprovalBot.Helpers
9 | {
10 | public class ApprovalStatusHelper
11 | {
12 | public static async Task> GetApprovalsForUserCard(string accessToken, string userId)
13 | {
14 | // Get all approvals requested by current user
15 | var approvals = await DatabaseHelper.GetApprovalsAsync(a => a.Requestor == userId);
16 |
17 | if (approvals.Count() == 0)
18 | {
19 | // Return a simple message
20 | return null;
21 | }
22 | else if (approvals.Count() == 1)
23 | {
24 | // Return status card of the only approval
25 | return new List() { GetApprovalStatusCard(approvals.First()) };
26 | }
27 | else
28 | {
29 | var approvalCardList = new List();
30 |
31 | foreach (var approval in approvals)
32 | {
33 | var approvalDetailCard = new AdaptiveCard();
34 |
35 | var approvalColumnSet = new AdaptiveColumnSet();
36 |
37 | // Get thumbnail
38 | var thumbnailUri = await GraphHelper.GetFileThumbnailDataUri(accessToken, approval.File.Id);
39 |
40 | var fileThumbCol = new AdaptiveColumn()
41 | {
42 | Width = AdaptiveColumnWidth.Auto.ToLower()
43 | };
44 |
45 | fileThumbCol.Items.Add(new AdaptiveImage()
46 | {
47 | Url = new Uri(thumbnailUri),
48 | AltText = "File thumbnail",
49 | Size = AdaptiveImageSize.Small
50 | });
51 |
52 | approvalColumnSet.Columns.Add(fileThumbCol);
53 |
54 | var fileNameCol = new AdaptiveColumn()
55 | {
56 | Width = AdaptiveColumnWidth.Stretch.ToLower()
57 | };
58 |
59 | fileNameCol.Items.Add(new AdaptiveTextBlock()
60 | {
61 | Size = AdaptiveTextSize.Medium,
62 | Text = approval.File.Name
63 | });
64 |
65 | fileNameCol.Items.Add(new AdaptiveTextBlock()
66 | {
67 | IsSubtle = true,
68 | Spacing = AdaptiveSpacing.None,
69 | Text = $"Requested: {TimeZoneHelper.GetAdaptiveDateTimeString(approval.RequestDate)}"
70 | });
71 |
72 | approvalColumnSet.Columns.Add(fileNameCol);
73 |
74 | approvalDetailCard.Body.Add(approvalColumnSet);
75 |
76 | approvalDetailCard.Actions.Add(new AdaptiveSubmitAction()
77 | {
78 | Title = "This one",
79 | DataJson = $@"{{ ""cardAction"": ""{CardActionTypes.SelectApproval}"", ""selectedApproval"": ""{approval.Id}"" }}"
80 | });
81 |
82 | approvalCardList.Add(approvalDetailCard);
83 | }
84 |
85 | return approvalCardList;
86 | }
87 | }
88 |
89 | public static async Task GetApprovalStatusCard(string approvalId)
90 | {
91 | var approval = await DatabaseHelper.GetApprovalAsync(approvalId);
92 | return GetApprovalStatusCard(approval);
93 | }
94 |
95 | public static AdaptiveCard GetApprovalStatusCard(Approval approval)
96 | {
97 | var statusCard = new AdaptiveCard();
98 |
99 | statusCard.Body.Add(new AdaptiveTextBlock()
100 | {
101 | Weight = AdaptiveTextWeight.Bolder,
102 | Size = AdaptiveTextSize.Medium,
103 | Text = $"Status for {approval.File.Name}"
104 | });
105 |
106 | foreach (var approver in approval.Approvers)
107 | {
108 | var approverColumnSet = new AdaptiveColumnSet();
109 |
110 | var emailAddressColumn = new AdaptiveColumn()
111 | {
112 | Width = AdaptiveColumnWidth.Stretch.ToLower()
113 | };
114 |
115 | emailAddressColumn.Items.Add(new AdaptiveTextBlock()
116 | {
117 | Text = approver.EmailAddress
118 | });
119 |
120 | approverColumnSet.Columns.Add(emailAddressColumn);
121 |
122 | var responseColumn = new AdaptiveColumn()
123 | {
124 | Width = AdaptiveColumnWidth.Auto.ToLower()
125 | };
126 |
127 | responseColumn.Items.Add(ResponseCardTextBlockFromResponse(approver.Response));
128 |
129 | approverColumnSet.Columns.Add(responseColumn);
130 |
131 | statusCard.Body.Add(approverColumnSet);
132 |
133 | if (!string.IsNullOrEmpty(approver.ResponseNote))
134 | {
135 | var notesContainer = new AdaptiveContainer()
136 | {
137 | Style = AdaptiveContainerStyle.Emphasis
138 | };
139 |
140 | notesContainer.Items.Add(new AdaptiveTextBlock()
141 | {
142 | Weight = AdaptiveTextWeight.Bolder,
143 | Text = "Note:"
144 | });
145 |
146 | notesContainer.Items.Add(new AdaptiveTextBlock()
147 | {
148 | Text = approver.ResponseNote,
149 | Wrap = true
150 | });
151 |
152 | statusCard.Body.Add(notesContainer);
153 | }
154 | }
155 |
156 | return statusCard;
157 | }
158 |
159 | private static AdaptiveTextBlock ResponseCardTextBlockFromResponse(ResponseStatus response)
160 | {
161 | var textBlock = new AdaptiveTextBlock();
162 |
163 | switch (response)
164 | {
165 | case ResponseStatus.Approved:
166 | textBlock.Color = AdaptiveTextColor.Good;
167 | textBlock.Text = "Approved";
168 | break;
169 | case ResponseStatus.Rejected:
170 | textBlock.Color = AdaptiveTextColor.Attention;
171 | textBlock.Text = "Rejected";
172 | break;
173 | default:
174 | textBlock.Color = AdaptiveTextColor.Warning;
175 | textBlock.Text = "Not responded";
176 | break;
177 | }
178 |
179 | return textBlock;
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/DatabaseHelper.cs:
--------------------------------------------------------------------------------
1 | using ApprovalBot.Models;
2 | using Microsoft.Azure.Documents;
3 | using Microsoft.Azure.Documents.Client;
4 | using Microsoft.Azure.Documents.Linq;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Configuration;
8 | using System.Linq;
9 | using System.Linq.Expressions;
10 | using System.Net;
11 | using System.Threading.Tasks;
12 |
13 | namespace ApprovalBot.Helpers
14 | {
15 | public static class DatabaseHelper
16 | {
17 | private static readonly string databaseUri = ConfigurationManager.AppSettings["DatabaseUri"];
18 | private static readonly string databaseKey = ConfigurationManager.AppSettings["DatabaseKey"];
19 | private static readonly string databaseName = "ApprovalBotDB";
20 | private static readonly string collectionName = "Approvals";
21 | private static readonly string loggingCollectionName = "GraphRequests";
22 |
23 | private static DocumentClient client = null;
24 |
25 | public static void Initialize()
26 | {
27 | client = new DocumentClient(new Uri(databaseUri), databaseKey);
28 | CreateDatabaseIfNotExistsAsync().Wait();
29 | CreateCollectionIfNotExistsAsync().Wait();
30 | }
31 |
32 | private static async Task CreateDatabaseIfNotExistsAsync()
33 | {
34 | try
35 | {
36 | await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(databaseName));
37 | }
38 | catch (DocumentClientException e)
39 | {
40 | if (e.StatusCode == HttpStatusCode.NotFound)
41 | {
42 | await client.CreateDatabaseAsync(new Database { Id = databaseName });
43 | }
44 | else
45 | {
46 | throw;
47 | }
48 | }
49 | }
50 |
51 | private static async Task CreateCollectionIfNotExistsAsync()
52 | {
53 | try
54 | {
55 | await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseName, collectionName));
56 | }
57 | catch (DocumentClientException e)
58 | {
59 | if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
60 | {
61 | await client.CreateDocumentCollectionAsync(
62 | UriFactory.CreateDatabaseUri(databaseName),
63 | new DocumentCollection { Id = collectionName },
64 | new RequestOptions { OfferThroughput = 1000 });
65 | }
66 | else
67 | {
68 | throw;
69 | }
70 | }
71 |
72 | try
73 | {
74 | await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseName, loggingCollectionName));
75 | }
76 | catch (DocumentClientException e)
77 | {
78 | if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
79 | {
80 | await client.CreateDocumentCollectionAsync(
81 | UriFactory.CreateDatabaseUri(databaseName),
82 | new DocumentCollection { Id = loggingCollectionName },
83 | new RequestOptions { OfferThroughput = 1000 });
84 | }
85 | else
86 | {
87 | throw;
88 | }
89 | }
90 | }
91 |
92 | public static async Task> GetApprovalsAsync(Expression> predicate)
93 | {
94 | IDocumentQuery query = client.CreateDocumentQuery(
95 | UriFactory.CreateDocumentCollectionUri(databaseName, collectionName))
96 | .Where(predicate)
97 | .AsDocumentQuery();
98 |
99 | List results = new List();
100 | while (query.HasMoreResults)
101 | {
102 | results.AddRange(await query.ExecuteNextAsync());
103 | }
104 |
105 | return results;
106 | }
107 |
108 | public static async Task GetApprovalAsync(string id)
109 | {
110 | try
111 | {
112 | Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(databaseName, collectionName, id));
113 | return (Approval)(dynamic)document;
114 | }
115 | catch (DocumentClientException e)
116 | {
117 | if (HttpStatusCode.NotFound == e.StatusCode)
118 | {
119 | return null;
120 | }
121 | else
122 | {
123 | throw;
124 | }
125 | }
126 | }
127 |
128 | public static async Task CreateApprovalAsync(Approval approval)
129 | {
130 | Document document = await client.CreateDocumentAsync(
131 | UriFactory.CreateDocumentCollectionUri(databaseName, collectionName),
132 | approval);
133 |
134 | return (Approval)(dynamic)document;
135 | }
136 |
137 | public static async Task UpdateApprovalAsync(string id, Approval approval)
138 | {
139 | Document document = await client.ReplaceDocumentAsync(
140 | UriFactory.CreateDocumentUri(databaseName, collectionName, id), approval);
141 |
142 | return (Approval)(dynamic)document;
143 | }
144 |
145 | public static async Task DeleteApprovalAsync(string id)
146 | {
147 | try
148 | {
149 | await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(databaseName, collectionName, id));
150 | }
151 | catch (DocumentClientException e)
152 | {
153 | if (e.StatusCode != HttpStatusCode.NotFound)
154 | {
155 | throw;
156 | }
157 | }
158 | }
159 |
160 | public static async Task DeleteAllUserApprovals(string userId)
161 | {
162 | var userApprovals = await GetApprovalsAsync(a => a.Requestor == userId);
163 |
164 | foreach (var approval in userApprovals)
165 | {
166 | await DeleteApprovalAsync(approval.Id);
167 | }
168 | }
169 |
170 | public static async Task AddGraphLog(GraphLogEntry entry)
171 | {
172 | Document document = await client.CreateDocumentAsync(
173 | UriFactory.CreateDocumentCollectionUri(databaseName, loggingCollectionName),
174 | entry);
175 |
176 | return document;
177 | }
178 | }
179 | }
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/EmailHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Mail;
3 |
4 | namespace ApprovalBot.Helpers
5 | {
6 | public static class EmailHelper
7 | {
8 | public static bool IsValidSmtpAddress(string address)
9 | {
10 | try
11 | {
12 | new MailAddress(address);
13 | return true;
14 | }
15 | catch (FormatException)
16 | {
17 | return false;
18 | }
19 | }
20 |
21 | public static string[] ConvertDelimitedAddressStringToArray(string addressString)
22 | {
23 | string[] addresses = addressString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
24 |
25 | foreach(string address in addresses)
26 | {
27 | if (!IsValidSmtpAddress(address))
28 | {
29 | return null;
30 | }
31 | }
32 |
33 | return addresses;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/GraphHelper.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using ApprovalBot.Models;
3 | using Microsoft.Bot.Builder.Dialogs;
4 | using Microsoft.Graph;
5 | using Newtonsoft.Json;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Configuration;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Net.Http;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | namespace ApprovalBot.Helpers
16 | {
17 | public static class GraphHelper
18 | {
19 | private static readonly bool LogGraphRequests =
20 | string.IsNullOrEmpty(ConfigurationManager.AppSettings["LogGraphRequests"]) ? false :
21 | Convert.ToBoolean(ConfigurationManager.AppSettings["LogGraphRequests"]);
22 |
23 | public static async Task GetFilePickerCardFromOneDrive(string accessToken)
24 | {
25 | var client = GetAuthenticatedClient(accessToken);
26 |
27 | // Get the first 20 items from the root of user's OneDrive
28 | var driveItems = await client.Me.Drive.Root.Children.Request()
29 | .Select("id,name,file")
30 | .Top(20)
31 | .GetAsync();
32 |
33 | var fileList = new List();
34 |
35 | while (driveItems != null)
36 | {
37 | foreach (var item in driveItems)
38 | {
39 | // Only process files
40 | if (item.File != null)
41 | {
42 | fileList.Add(new AdaptiveChoice()
43 | {
44 | Title = item.Name,
45 | Value = item.Id
46 | });
47 | }
48 | }
49 |
50 | if (driveItems.NextPageRequest != null)
51 | {
52 | driveItems = await driveItems.NextPageRequest.GetAsync();
53 | }
54 | else
55 | {
56 | driveItems = null;
57 | }
58 | }
59 |
60 | var pickerCard = new AdaptiveCard();
61 |
62 | if (fileList.Count > 0)
63 | {
64 | pickerCard.Body.Add(new AdaptiveTextBlock()
65 | {
66 | Text = "Get approval for which file?",
67 | Size = AdaptiveTextSize.Large,
68 | Weight = AdaptiveTextWeight.Bolder
69 | });
70 |
71 | pickerCard.Body.Add(new AdaptiveTextBlock()
72 | {
73 | Text = "I found these in your OneDrive",
74 | Weight = AdaptiveTextWeight.Bolder
75 | });
76 |
77 | pickerCard.Body.Add(new AdaptiveChoiceSetInput()
78 | {
79 | Id = "selectedFile",
80 | IsMultiSelect = false,
81 | Style = AdaptiveChoiceInputStyle.Compact,
82 | Choices = fileList
83 | });
84 |
85 | pickerCard.Actions.Add(new AdaptiveSubmitAction()
86 | {
87 | Title = "OK",
88 | DataJson = $@"{{ ""cardAction"": ""{CardActionTypes.SelectFile}"" }}"
89 | });
90 |
91 | return pickerCard;
92 | }
93 |
94 | return null;
95 | }
96 |
97 | public static async Task GetFileDetailCard(string accessToken, string fileId)
98 | {
99 | var client = GetAuthenticatedClient(accessToken);
100 |
101 | // Get the file with thumbnails
102 | var file = await client.Me.Drive.Items[fileId].Request()
103 | .Expand("thumbnails")
104 | .GetAsync();
105 |
106 | // Get people user interacts with regularly
107 | var potentialApprovers = await client.Me.People.Request()
108 | // Only want organizational users, and do not want to send back to bot
109 | .Filter("personType/subclass eq 'OrganizationUser' and displayName ne 'Approval Bot'")
110 | .Top(10)
111 | .GetAsync();
112 |
113 | var fileCard = new AdaptiveCard();
114 |
115 | fileCard.Body.Add(new AdaptiveTextBlock()
116 | {
117 | Text = "Get approval for this file?",
118 | Size = AdaptiveTextSize.Large,
119 | Weight = AdaptiveTextWeight.Bolder
120 | });
121 |
122 | fileCard.Body.Add(new AdaptiveTextBlock()
123 | {
124 | Text = file.Name,
125 | Weight = AdaptiveTextWeight.Bolder
126 | });
127 |
128 | fileCard.Body.Add(new AdaptiveFactSet()
129 | {
130 | Facts = new List()
131 | {
132 | new AdaptiveFact("Size", $"{file.Size / 1024} KB"),
133 | new AdaptiveFact("Last modified", TimeZoneHelper.GetAdaptiveDateTimeString(file.LastModifiedDateTime.Value)),
134 | new AdaptiveFact("Last modified by", file.LastModifiedBy.User.DisplayName)
135 | }
136 | });
137 |
138 | if (file.Thumbnails != null && file.Thumbnails.Count > 0)
139 | {
140 | fileCard.Body.Add(new AdaptiveImage()
141 | {
142 | Size = AdaptiveImageSize.Stretch,
143 | AltText = "File thumbnail",
144 | Url = new Uri(file.Thumbnails[0].Large.Url)
145 | });
146 | }
147 |
148 | var recipientPromptCard = new AdaptiveCard();
149 |
150 | recipientPromptCard.Body.Add(new AdaptiveTextBlock()
151 | {
152 | Text = "Who should I ask for approval?",
153 | Weight = AdaptiveTextWeight.Bolder
154 | });
155 |
156 | var recipientPicker = new AdaptiveChoiceSetInput()
157 | {
158 | Id = "approvers",
159 | IsMultiSelect = true,
160 | Style = AdaptiveChoiceInputStyle.Compact
161 | };
162 |
163 | foreach(var potentialApprover in potentialApprovers)
164 | {
165 | recipientPicker.Choices.Add(new AdaptiveChoice()
166 | {
167 | Title = potentialApprover.DisplayName,
168 | Value = potentialApprover.ScoredEmailAddresses.First().Address
169 | });
170 | }
171 |
172 | recipientPromptCard.Body.Add(recipientPicker);
173 |
174 | recipientPromptCard.Actions.Add(new AdaptiveSubmitAction()
175 | {
176 | Title = "Send Request",
177 | DataJson = $@"{{ ""cardAction"": ""{CardActionTypes.SendApprovalRequest}"", ""selectedFile"": ""{fileId}"" }}"
178 | });
179 |
180 | fileCard.Actions.Add(new AdaptiveShowCardAction()
181 | {
182 | Title = "Yes",
183 | Card = recipientPromptCard
184 | });
185 |
186 | fileCard.Actions.Add(new AdaptiveSubmitAction()
187 | {
188 | Title = "No",
189 | DataJson = $@"{{ ""cardAction"": ""{CardActionTypes.WrongFile}"" }}"
190 | });
191 |
192 | return fileCard;
193 | }
194 |
195 | public static async Task GetFileInfo(string accessToken, string fileId)
196 | {
197 | var client = GetAuthenticatedClient(accessToken);
198 |
199 | // Get the file with thumbnails
200 | var file = await client.Me.Drive.Items[fileId].Request()
201 | .Select("id,name")
202 | .Expand("thumbnails")
203 | .GetAsync();
204 |
205 | // Get a sharing link
206 | var sharingLink = await client.Me.Drive.Items[fileId]
207 | .CreateLink("view", "organization")
208 | .Request()
209 | .PostAsync();
210 |
211 | return new ApprovalFileInfo()
212 | {
213 | Id = fileId,
214 | Name = file.Name,
215 | SharingUrl = sharingLink.Link.WebUrl,
216 | ThumbnailUrl = (file.Thumbnails != null && file.Thumbnails.Count > 0) ? file.Thumbnails[0].Small.Url : null
217 | };
218 | }
219 |
220 | public static async Task GetUser(string accessToken)
221 | {
222 | var client = GetAuthenticatedClient(accessToken);
223 |
224 | return await client.Me.Request()
225 | .Select("displayName, mail")
226 | .GetAsync();
227 | }
228 |
229 | public static async Task GetUserPhotoDataUri(string accessToken, string size)
230 | {
231 | var client = GetAuthenticatedClient(accessToken);
232 |
233 | try
234 | {
235 | var photo = await client.Me.Photos[size].Content.Request().GetAsync();
236 |
237 | var photoStream = new MemoryStream();
238 | photo.CopyTo(photoStream);
239 |
240 | var photoBytes = photoStream.ToArray();
241 |
242 | return string.Format("data:image/png;base64,{0}",
243 | Convert.ToBase64String(photoBytes));
244 | }
245 | catch (Exception)
246 | {
247 | return null;
248 | }
249 | }
250 |
251 | public static async Task GetFileThumbnailDataUri(string accessToken, string fileId)
252 | {
253 | var client = GetAuthenticatedClient(accessToken);
254 |
255 | try
256 | {
257 | var thumbnail = await client.Me.Drive.Items[fileId].Thumbnails["0"]["small"].Content.Request().GetAsync();
258 |
259 | var thumbnailStream = new MemoryStream();
260 | thumbnail.CopyTo(thumbnailStream);
261 |
262 | var thumbnailBytes = thumbnailStream.ToArray();
263 |
264 | return string.Format("data:image/png;base64,{0}",
265 | Convert.ToBase64String(thumbnailBytes));
266 | }
267 | catch (Exception)
268 | {
269 | return null;
270 | }
271 | }
272 |
273 | public static async Task SendRequestCard(string accessToken, AdaptiveCard card, string recipient, string sender)
274 | {
275 | var toRecipient = new Recipient()
276 | {
277 | EmailAddress = new EmailAddress() { Address = recipient }
278 | };
279 |
280 | var actionableMessage = new Message()
281 | {
282 | Subject = "Request for release approval",
283 | ToRecipients = new List() { toRecipient },
284 | Body = new ItemBody()
285 | {
286 | ContentType = BodyType.Html,
287 | Content = HtmlBodyFromCard(card)
288 | }
289 | };
290 |
291 | if (!string.IsNullOrEmpty(sender))
292 | {
293 | actionableMessage.From = new Recipient() {
294 | EmailAddress = new EmailAddress() { Address = sender }
295 | };
296 | }
297 |
298 | var client = GetAuthenticatedClient(accessToken);
299 |
300 | await client.Me.SendMail(actionableMessage, true).Request().PostAsync();
301 | }
302 |
303 | private const string htmlBodyTemplate = @"
304 |
305 |
306 |
309 |
310 |
311 | You've received a request for file approval. If you cannot see the approval card in this message, please visit the
Approval Portal to respond to this request.
312 |
313 | ";
314 |
315 | private static string HtmlBodyFromCard(AdaptiveCard card)
316 | {
317 | var cardPayload = JsonConvert.SerializeObject(card);
318 | return string.Format(htmlBodyTemplate, cardPayload);
319 | }
320 |
321 | private static GraphServiceClient GetAuthenticatedClient(string accessToken)
322 | {
323 | if (LogGraphRequests)
324 | return new GraphServiceClient(new DelegateAuthenticationProvider(
325 | async (requestMessage) =>
326 | {
327 | requestMessage.Headers.Authorization =
328 | new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
329 | await Task.FromResult(0);
330 | }),
331 | new HttpProvider(new LoggingHttpProvider(), true, null));
332 |
333 | return new GraphServiceClient(new DelegateAuthenticationProvider(
334 | async (requestMessage) =>
335 | {
336 | requestMessage.Headers.Authorization =
337 | new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
338 | await Task.FromResult(0);
339 | }));
340 | }
341 | }
342 |
343 | public class LoggingHttpProvider : HttpClientHandler
344 | {
345 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
346 | {
347 | // Log request
348 | string requestId = Guid.NewGuid().ToString();
349 | request.Headers.Add("client-request-id", requestId);
350 | var requestLog = new GraphLogEntry(request);
351 | await requestLog.LoadBody(request);
352 | await DatabaseHelper.AddGraphLog(requestLog);
353 |
354 | try
355 | {
356 | HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
357 |
358 | // Log response
359 | var responseLog = new GraphLogEntry(response);
360 | await responseLog.LoadBody(response);
361 | await DatabaseHelper.AddGraphLog(responseLog);
362 |
363 | return response;
364 | }
365 | catch (Exception ex)
366 | {
367 | await DatabaseHelper.AddGraphLog(new GraphLogEntry(ex, requestId));
368 | throw;
369 | }
370 | }
371 | }
372 |
373 | public class GraphLogEntry
374 | {
375 | public string RequestId { get; set; }
376 | public string RequestUrl { get; set; }
377 | public string RequestMethod { get; set; }
378 | public string Body { get; set; }
379 |
380 | public GraphLogEntry(HttpRequestMessage request)
381 | {
382 | RequestId = request.Headers.Where(h => string.Equals(h.Key, "client-request-id", StringComparison.OrdinalIgnoreCase)).FirstOrDefault().Value?.FirstOrDefault();
383 | RequestUrl = request.RequestUri.ToString();
384 | RequestMethod = request.Method.ToString();
385 | }
386 |
387 | public GraphLogEntry(HttpResponseMessage response)
388 | {
389 | RequestId = response.Headers.Where(h => string.Equals(h.Key, "client-request-id", StringComparison.OrdinalIgnoreCase)).FirstOrDefault().Value?.FirstOrDefault();
390 | }
391 |
392 | public GraphLogEntry(Exception ex, string requestId)
393 | {
394 | RequestId = requestId;
395 | RequestMethod = "EXCEPTION";
396 | Body = ex.ToString();
397 | }
398 |
399 | public async Task LoadBody(HttpRequestMessage request)
400 | {
401 | if (request.Content != null)
402 | {
403 | Body = await request.Content.ReadAsStringAsync();
404 | }
405 | }
406 |
407 | public async Task LoadBody(HttpResponseMessage response)
408 | {
409 | if (response.Content != null)
410 | {
411 | Body = await response.Content.ReadAsStringAsync();
412 | }
413 | }
414 | }
415 | }
--------------------------------------------------------------------------------
/ApprovalBot/Helpers/TimeZoneHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 |
6 | namespace ApprovalBot.Helpers
7 | {
8 | public static class TimeZoneHelper
9 | {
10 | public static string GetAdaptiveDateTimeString(DateTimeOffset dateTimeOffset)
11 | {
12 | var rfc3389String = $"{dateTimeOffset.UtcDateTime.ToString("s")}Z";
13 |
14 | // Returns string like
15 | // {{DATE(2018-04-26T07:00:00Z,SHORT)}} {{TIME(2018-04-26T07:00:00Z)}}
16 | // See docs at https://docs.microsoft.com/en-us/adaptive-cards/create/textfeatures#datetime-function-rules
17 | return $"{{{{DATE({rfc3389String},SHORT)}}}} {{{{TIME({rfc3389String})}}}}";
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/ApprovalBot/Models/ActionData.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Web;
6 |
7 | namespace ApprovalBot.Models
8 | {
9 | public static class CardActionTypes
10 | {
11 | public static string SelectFile = "selectFile";
12 | public static string WrongFile = "wrongFile";
13 | public static string SendApprovalRequest = "sendApproval";
14 | public static string SelectApproval = "selectApproval";
15 | }
16 |
17 | public class ActionData
18 | {
19 | public string CardAction { get; set; }
20 | public string SelectedFile { get; set; }
21 | public string Approvers { get; set; }
22 | public string SelectedApproval { get; set; }
23 |
24 | public static ActionData Parse(object obj)
25 | {
26 | return ((JToken)obj).ToObject();
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/ApprovalBot/Models/ActionableEmailResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 |
6 | namespace ApprovalBot.Models
7 | {
8 | public class ActionableEmailResponse
9 | {
10 | public string UserEmail { get; set; }
11 | public string ApprovalId { get; set; }
12 | public string Response { get; set; }
13 | public string Notes { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/ApprovalBot/Models/Approval.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace ApprovalBot.Models
6 | {
7 | public class Approval
8 | {
9 | [JsonProperty(PropertyName = "id")]
10 | public string Id { get; set; }
11 | [JsonProperty(PropertyName = "requestor")]
12 | public string Requestor { get; set; }
13 | [JsonProperty(PropertyName = "file")]
14 | public ApprovalFileInfo File { get; set; }
15 | [JsonProperty(PropertyName = "approvers")]
16 | public List Approvers { get; set; }
17 | [JsonProperty(PropertyName = "requestDate")]
18 | public DateTimeOffset RequestDate { get; set; }
19 | }
20 | }
--------------------------------------------------------------------------------
/ApprovalBot/Models/ApproverInfo.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Web;
6 |
7 | namespace ApprovalBot.Models
8 | {
9 | public enum ResponseStatus
10 | {
11 | NotResponded,
12 | Approved,
13 | Rejected
14 | }
15 |
16 | public class ApproverInfo
17 | {
18 | [JsonProperty(PropertyName = "emailAddress")]
19 | public string EmailAddress { get; set; }
20 | [JsonProperty(PropertyName = "response")]
21 | public ResponseStatus Response { get; set; }
22 | [JsonProperty(PropertyName = "responseNote")]
23 | public string ResponseNote { get; set; }
24 | }
25 | }
--------------------------------------------------------------------------------
/ApprovalBot/Models/FileInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 |
6 | namespace ApprovalBot.Models
7 | {
8 | public class ApprovalFileInfo
9 | {
10 | public string Id { get; set; }
11 | public string Name { get; set; }
12 | public string SharingUrl { get; set; }
13 | public string ThumbnailUrl { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/ApprovalBot/OutlookAdaptive/AdaptiveActionSet.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using Newtonsoft.Json;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 |
6 | namespace ApprovalBot.OutlookAdaptive
7 | {
8 | public class AdaptiveActionSet : AdaptiveElement
9 | {
10 | public const string TypeName = "ActionSet";
11 |
12 | public override string Type { get; set; } = TypeName;
13 |
14 | public AdaptiveActionSet(){}
15 |
16 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
17 | [DefaultValue(typeof(AdaptiveHorizontalAlignment), "left")]
18 | public AdaptiveHorizontalAlignment HorizontalAlignment { get; set; }
19 |
20 | [JsonRequired]
21 | public List Actions { get; set; } = new List();
22 | }
23 | }
--------------------------------------------------------------------------------
/ApprovalBot/OutlookAdaptive/AdaptiveHttpAction.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using Newtonsoft.Json;
3 | using Newtonsoft.Json.Converters;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.ComponentModel;
7 |
8 | namespace ApprovalBot.OutlookAdaptive
9 | {
10 | [JsonConverter(typeof(StringEnumConverter), true)]
11 | public enum AdaptiveHttpActionMethod
12 | {
13 | GET,
14 | POST
15 | }
16 |
17 | public class AdaptiveHttpActionHeader
18 | {
19 | [JsonRequired]
20 | public string Name { get; set; }
21 |
22 | [JsonRequired]
23 | public string Value { get; set; }
24 | }
25 |
26 | public class AdaptiveHttpAction : AdaptiveAction
27 | {
28 | public const string TypeName = "Action.Http";
29 |
30 | public override string Type { get; set; } = TypeName;
31 |
32 | [JsonRequired]
33 | public AdaptiveHttpActionMethod Method { get; set; }
34 |
35 | [JsonRequired]
36 | public Uri Url { get; set; }
37 |
38 | [DefaultValue(null)]
39 | public List Headers { get; set; }
40 |
41 | [DefaultValue(null)]
42 | public string Body { get; set; }
43 | }
44 | }
--------------------------------------------------------------------------------
/ApprovalBot/OutlookAdaptive/AdaptiveToggleVisibilityAction.cs:
--------------------------------------------------------------------------------
1 | using AdaptiveCards;
2 | using Newtonsoft.Json;
3 |
4 | namespace ApprovalBot.OutlookAdaptive
5 | {
6 | public class AdaptiveToggleVisibilityAction : AdaptiveAction
7 | {
8 | public const string TypeName = "Action.ToggleVisibility";
9 |
10 | public override string Type { get; set; } = TypeName;
11 |
12 | [JsonRequired]
13 | public string[] TargetElements { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/ApprovalBot/PrivateSettings.example.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ApprovalBot/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("ApprovalBot")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("ApprovalBot")]
13 | [assembly: AssemblyCopyright("Copyright © 2016")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("4e426b56-d589-44ea-b7a1-72ed5343b354")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Revision and Build Numbers
33 | // by using the '*' as shown below:
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/ApprovalBot/Web.Debug.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
29 |
30 |
--------------------------------------------------------------------------------
/ApprovalBot/Web.Release.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
19 |
30 |
31 |
--------------------------------------------------------------------------------
/ApprovalBot/Web.config:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/ApprovalBot/default.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ApprovalBot
9 | Describe your bot here and your terms of use etc.
10 | Visit Bot Framework to register your bot. When you register it, remember to set your bot's endpoint to
https://your_bots_hostname /api/messages
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ApprovalBot/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | products:
4 | - office-outlook
5 | - office-onedrive
6 | - ms-graph
7 | languages:
8 | - csharp
9 | description: "A sample Bot that uses adaptive cards and the .NET Graph SDK to send actionable messages requesting approval to release files on OneDrive."
10 | extensions:
11 | contentType: samples
12 | technologies:
13 | - Microsoft Graph
14 | - Microsoft Bot Framework
15 | services:
16 | - Outlook
17 | - OneDrive
18 | createdDate: 4/23/2018 12:12:07 PM
19 | ---
20 | # Approval Bot Sample
21 |
22 | ## Running locally
23 |
24 | Follow these steps to enable running the bot locally for debugging.
25 |
26 | ### Prerequisites
27 |
28 | - [ngrok](https://ngrok.com/)
29 | - [Bot Framework Emulator](https://github.com/Microsoft/BotFramework-Emulator/releases)
30 | - [Azure Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
31 | - Visual Studio 2017
32 |
33 | ### Register the app
34 |
35 | 1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com). Login using a **Work or School Account**.
36 |
37 | 1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations (Preview)** under **Manage**.
38 |
39 | 
40 |
41 | 1. Select **New registration**. On the **Register an application** page, set the values as follows.
42 |
43 | - Set a preferred **Name** e.g. `Approval Bot`.
44 | - Set **Supported account types** to **Accounts in any organizational directory**.
45 | - Under **Redirect URI**, set the first drop-down to `Web` and set the value to `http://localhost:3979/callback`.
46 |
47 | 
48 |
49 | 1. Choose **Register**. On the **Approval Bot** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step.
50 |
51 | 
52 |
53 | 1. Select **Certificates & secrets** under **Manage**. Select the **New client secret** button. Enter a value in **Description** and select one of the options for **Expires** and choose **Add**.
54 |
55 | 
56 |
57 | 1. Copy the client secret value before you leave this page. You will need it in the next step.
58 |
59 | > [!IMPORTANT]
60 | > This client secret is never shown again, so make sure you copy it now.
61 |
62 | 
63 |
64 | ### Set up the ngrok proxy
65 |
66 | You must expose a public HTTPS endpoint to receive notifications from the Bot Framework Emulator. While testing, you can use ngrok to temporarily allow messages from the Bot Framework Emulator to tunnel to a *localhost* port on your computer.
67 |
68 | You can use the ngrok web interface ([http://127.0.0.1:4040](http://127.0.0.1:4040)) to inspect the HTTP traffic that passes through the tunnel. To learn more about using ngrok, see the [ngrok website](https://ngrok.com/).
69 |
70 |
71 | 1. [Download ngrok](https://ngrok.com/download) for Windows.
72 |
73 | 1. Unzip the package and run ngrok.exe.
74 |
75 | 1. Run the following command line on the ngrok console:
76 |
77 | ```Shell
78 | ngrok http 3979 --host-header=localhost:3979
79 | ```
80 |
81 | 
82 |
83 | 1. Copy the HTTPS URL that's shown in the console. You'll use this to configure your `NgrokRootUrl` in the sample.
84 |
85 | 
86 |
87 | > **Note:** Keep the console open while testing. If you close it, the tunnel also closes and you'll need to generate a new URL and update the sample.
88 |
89 | ### Configure and run the sample
90 |
91 | 1. Clone the repository locally.
92 | 1. Make a copy of the **./ApprovalBot/PrivateSettings.example.config** file in the same directory, and name the copy `PrivateSettings.config`.
93 | 1. Open **ApprovalBot.sln** in Visual Studio, then open the **PrivateSettings.config** file.
94 |
95 | 1. Set the value of `MicrosoftAppId` to the Application (client) ID you generated in the previous step, and set the value of `MicrosoftAppPassword` to the secret you generated afterwards.
96 |
97 | 1. Paste the ngrok HTTPS URL value copied from the previous step into the value of `NgrokRootUrl` in **PrivateSettings.config**, and save your changes.
98 |
99 | > **IMPORTANT**: Leave ngrok running while you run the sample. If you stop ngrok and re-start it, the forwarding URL changes, and you'll need to update the value of `NgrokRootUrl`.
100 |
101 | 1. Start the Azure Cosmos DB Emulator. This needs to be running before you start the sample.
102 |
103 | 1. Press F5 to debug the sample.
104 |
105 | 1. Run the Bot Framework Emulator. At the top, where it says **Enter your endpoint URL**, enter `https://localhost:3979/api/messages`.
106 |
107 | 1. That will prompt for app ID and password. Enter your app ID and secret, and leave **Locale** blank.
108 |
109 | 
110 |
111 | 1. Click **Connect**.
112 |
113 | 1. Send `hi` to confirm the connection.
114 |
115 | 
116 |
117 | ## Limitations when running locally
118 |
119 | When running the sample locally, the approval request email that sent is a little different. Because it's not running on a confirmed, registered domain, we must send the message to and from the same account. What that means is that when you get to the point where you request approval, you must include yourself in the list of approvers.
120 |
121 | You can include other approvers, but the message they receive won't show the adaptive card. Login to your own mailbox with Outlook to test the adaptive card.
122 |
123 | ## Contributing
124 |
125 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
126 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
127 | the rights to use your contribution. For details, visit https://cla.microsoft.com.
128 |
129 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
130 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
131 | provided by the bot. You will only need to do this once across all repos using our CLA.
132 |
133 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
134 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
135 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
136 |
137 | ## Copyright
138 |
139 | Copyright (c) 2019 Microsoft. All rights reserved.
140 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/readme-images/aad-application-id.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/aad-application-id.PNG
--------------------------------------------------------------------------------
/readme-images/aad-copy-client-secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/aad-copy-client-secret.png
--------------------------------------------------------------------------------
/readme-images/aad-new-client-secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/aad-new-client-secret.png
--------------------------------------------------------------------------------
/readme-images/aad-portal-app-registrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/aad-portal-app-registrations.png
--------------------------------------------------------------------------------
/readme-images/aad-register-an-app.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/aad-register-an-app.PNG
--------------------------------------------------------------------------------
/readme-images/configure-emulator.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/configure-emulator.PNG
--------------------------------------------------------------------------------
/readme-images/hello-bot.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/hello-bot.PNG
--------------------------------------------------------------------------------
/readme-images/ngrok1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/ngrok1.PNG
--------------------------------------------------------------------------------
/readme-images/ngrok2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/botframework-csharp-approvalbot-sample/172034b3d80935efce98094f2ebb2971812d9da9/readme-images/ngrok2.PNG
--------------------------------------------------------------------------------