├── .env.example
├── .gitignore
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── Webex Meetings API Environment.postman_environment.json
├── Webex Meetings XML API.postman_collection.json
├── oauth2.py
├── requirements.txt
└── sampleFlow.py
/.env.example:
--------------------------------------------------------------------------------
1 | # (Common) Modify the below credentials to match your Webex Meeting site, and host Webex ID
2 |
3 | SITENAME=
4 | WEBEXID=
5 |
6 | # (sampleFlow.py) For Control Hub managed sites, you can retrieve a Webex Teams
7 | # personal access token via: https://developer.webex.com/docs/api/getting-started
8 | # For Webex sites without SSO enabled, provide a password. If both
9 | # are provided, the sample will attempt to use the access token
10 |
11 | ACCESS_TOKEN=
12 | PASSWORD=
13 |
14 | # (oauth2.py) Credentials for use with Control Hub managed SSO-enabled sites
15 |
16 | # User OAuth authentication method
17 | # Options: MEETINGS,TEAMS
18 | OAUTH_TYPE=
19 |
20 | CLIENT_ID=
21 | CLIENT_SECRET=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/*
2 | !.vscode/launch.json
3 | __pycache__/
4 | *.pem
5 | .env*
6 | !.env.example
7 | venv/*
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch sampleFlow.py",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/sampleFlow.py",
12 | "console": "integratedTerminal"
13 | },
14 | {
15 | "name": "Launch oauth2.py",
16 | "type": "python",
17 | "request": "launch",
18 | "module": "flask",
19 | "console": "integratedTerminal",
20 | "env": {
21 | "FLASK_APP": "oauth2.py"
22 | },
23 | "args": [
24 | "run",
25 | "--cert=cert.pem",
26 | "--key=key.pem"
27 | ]
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Cisco Systems, Inc. and/or its affiliates
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 | # webex-meetings-api-samples
2 |
3 | ## Overview
4 |
5 | This project contains sample scripts demonstrating usage of the Webex Meetings API, using Python.
6 |
7 | https://developer.cisco.com/webex-meetings/
8 |
9 | The concepts and techniques shown can be extended to enable automated management of Webex Meetings features.
10 |
11 | Also included is a Postman collection covering the requests used in the sample.
12 |
13 | Samples built/tested using [Visual Studio Code](https://code.visualstudio.com/).
14 |
15 | ## Available samples
16 |
17 | * `sampleFlow.py` - demonstrates the following work flow:
18 |
19 | * AuthenticateUser
20 | * GetUser
21 | * CreateMeeting
22 | * LstsummaryMeeting
23 | * GetMeeting
24 | * DelMeeting
25 |
26 | Can use webExId/password or webExId/accessToken for authorization
27 |
28 | * `oauth2.py` - demonstrates a web application that can perform a Webex Meetings OAuth2 login (using [Authlib](https://github.com/lepture/authlib)), then performs a GetUser request. Can use either [Webex Meetings OAuth](https://developer.cisco.com/docs/webex-meetings/#!integration) or [Webex Teams OAuth](https://developer.webex.com/docs/integrations) mechanisms.
29 |
30 | * `Postman collection - Webex Meetings XML API.json` - import this [Postman collection](https://learning.getpostman.com/docs/postman/collections/intro_to_collections/) which contains select scripted API request samples
31 |
32 | ## Webex environments
33 |
34 | * **Full site** - For full admin access and complete features, a production Webex instance is best. Some samples require site admin credentials
35 |
36 | * **Trial site** - The next best thing is to request a [free Webex trial](https://www.webex.com/pricing/free-trial.html) which should provide you admin access and lots of features to try
37 |
38 | * **DevNet Webex Sandbox** - For instant/free access, you can create an end-user account in the [DevNet Webex Sandbox](https://devnetsandbox.cisco.com/RM/Diagram/Index/b0547ab9-20cd-4a2d-a817-5c3b76258c83?diagramType=Topology). Note, this instance is non-SSO/CI, and uses username/password authentication
39 |
40 | ## Getting started
41 |
42 | * Install Python 3:
43 |
44 | On Windows, choose the option to add to PATH environment variable
45 |
46 | * From a terminal, clone this repo and change into the directory:
47 |
48 | ```bash
49 | git clone https://github.com/CiscoDevNet/webex-meetings-python-samples
50 | cd webex-meetings-python-samples
51 | ```
52 |
53 | * (Optional but recommended) Create a Python virtual environment named `venv`:
54 |
55 | (you may need to use `python3` on Linux/Mac):
56 |
57 | ```bash
58 | python -m venv venv
59 | source venv/bin/activate
60 | ```
61 |
62 | * Dependency Installation (you may need to use `pip3` on Linux/Mac)
63 |
64 | ```bash
65 | pip install -r requirements.txt
66 | ```
67 |
68 | * Open the project in Visual Studio Code:
69 |
70 | ```bash
71 | code .
72 | ```
73 |
74 | * In VS Code:
75 |
76 | 1. Rename `.env.example` to `.env`
77 |
78 | Edit `.env` to specify your Webex credentials
79 |
80 | 1. Click on the Debug tab, then the Launch configurations drop-down in the upper left.
81 |
82 | Select the sample you wish to run, e.g. 'Python: Launch sampleFlow.py`
83 |
84 | 1. See comments in the individual samples for additional sample-specific setup/launch details
85 |
86 | 1. Click the green **Start Debugging** button
87 |
88 |
--------------------------------------------------------------------------------
/Webex Meetings API Environment.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "dceee35c-13d3-437a-8ab3-a9841307c99c",
3 | "name": "Webex Meetings API Environment",
4 | "values": [
5 | {
6 | "key": "SITE_NAME",
7 | "value": "",
8 | "enabled": true
9 | },
10 | {
11 | "key": "WEBEXID",
12 | "value": "",
13 | "enabled": true
14 | },
15 | {
16 | "key": "PASSWORD",
17 | "value": "",
18 | "enabled": true
19 | },
20 | {
21 | "key": "_SESSION_TICKET",
22 | "value": "",
23 | "enabled": true
24 | },
25 | {
26 | "key": "_MEETING_START_DATE",
27 | "value": "",
28 | "enabled": true
29 | },
30 | {
31 | "key": "_MEETING_KEY",
32 | "value": "",
33 | "enabled": true
34 | }
35 | ],
36 | "_postman_variable_scope": "environment",
37 | "_postman_exported_at": "2020-04-06T19:49:56.806Z",
38 | "_postman_exported_using": "Postman/7.21.1"
39 | }
--------------------------------------------------------------------------------
/Webex Meetings XML API.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "ff384231-c299-4c5d-95f2-ce0d9033e4b8",
4 | "name": "Webex Meetings XML API",
5 | "description": "An collection of common Webex Meeting XML API requests.\n\nhttps://developer.cisco.com/webex-meetings/\n\nRequests can use Postman environment variables for common values to avoid updating each request manually. E.g. create a Postman environment with the following variables populated from your Webex site/user: SITE_NAME, WEBEXID, PASSWORD. Select this environment as the current active environment, and you can execute individual requests - variable names with double curly braces will be inserted automatically.\n\nThe entire collection can be run sequentially, and as a Postman test suite. Simply create an environment and populate as above, and use the Postman test runner against the root of the collection. Dynamic values like sessionTicket and meetingkey will be extracted and used in subsequent requests.\n",
6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7 | },
8 | "item": [
9 | {
10 | "name": "User Service",
11 | "item": [
12 | {
13 | "name": "AuthenticateUser (password)",
14 | "event": [
15 | {
16 | "listen": "test",
17 | "script": {
18 | "id": "820bda34-3944-4a4a-a89d-8da62f149c47",
19 | "exec": [
20 | "if (responseCode.code != 200) {",
21 | " ",
22 | " tests[\"AuthenticateUser: failed\"] = false",
23 | "}",
24 | "else {",
25 | " ",
26 | " jsonData = xml2Json(responseBody)",
27 | " ",
28 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
29 | " ",
30 | " tests[\"AuthenticateUser: failed\"] = false",
31 | " }",
32 | " else {",
33 | " ",
34 | " tests[\"AuthenticateUser: SUCCESS\"] = true",
35 | " ",
36 | " pm.environment.set( \"_SESSION_TICKET\", jsonData['serv:message']['serv:body']['serv:bodyContent']['use:sessionTicket'] );",
37 | " }",
38 | "}"
39 | ],
40 | "type": "text/javascript"
41 | }
42 | }
43 | ],
44 | "request": {
45 | "method": "POST",
46 | "header": [
47 | {
48 | "key": "Content-Type",
49 | "value": "application/xml",
50 | "type": "text"
51 | }
52 | ],
53 | "body": {
54 | "mode": "raw",
55 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{PASSWORD}}\n \n \n \n \n \n \n\n"
56 | },
57 | "url": {
58 | "raw": "https://api.webex.com/WBXService/XMLService",
59 | "protocol": "https",
60 | "host": [
61 | "api",
62 | "webex",
63 | "com"
64 | ],
65 | "path": [
66 | "WBXService",
67 | "XMLService"
68 | ]
69 | }
70 | },
71 | "response": []
72 | },
73 | {
74 | "name": "GetUser",
75 | "event": [
76 | {
77 | "listen": "test",
78 | "script": {
79 | "id": "6d549514-a3b4-4704-bede-5e6ea35c6856",
80 | "exec": [
81 | "if (responseCode.code != 200) {",
82 | " ",
83 | " tests[\"GetUser: failed\"] = false",
84 | "}",
85 | "else {",
86 | " ",
87 | " jsonData = xml2Json(responseBody)",
88 | " ",
89 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
90 | " ",
91 | " tests[\"GetUser: failed\"] = false",
92 | " }",
93 | " else {",
94 | " ",
95 | " tests[\"GetUser: SUCCESS\"] = true",
96 | " }",
97 | "}"
98 | ],
99 | "type": "text/javascript"
100 | }
101 | }
102 | ],
103 | "request": {
104 | "method": "POST",
105 | "header": [
106 | {
107 | "key": "Content-Type",
108 | "value": "application/xml",
109 | "type": "text"
110 | }
111 | ],
112 | "body": {
113 | "mode": "raw",
114 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{_SESSION_TICKET}} \n \n \n \n \n \t{{WEBEXID}}\n \n \n\n"
115 | },
116 | "url": {
117 | "raw": "https://api.webex.com/WBXService/XMLService",
118 | "protocol": "https",
119 | "host": [
120 | "api",
121 | "webex",
122 | "com"
123 | ],
124 | "path": [
125 | "WBXService",
126 | "XMLService"
127 | ]
128 | }
129 | },
130 | "response": []
131 | },
132 | {
133 | "name": "GetLoginTicket",
134 | "event": [
135 | {
136 | "listen": "test",
137 | "script": {
138 | "id": "b9b928c9-d6aa-4ef9-9608-2e3e4128e103",
139 | "exec": [
140 | "if (responseCode.code != 200) {",
141 | " ",
142 | " tests[\"GetLoginTicket: failed\"] = false",
143 | "}",
144 | "else {",
145 | " ",
146 | " jsonData = xml2Json(responseBody)",
147 | " ",
148 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
149 | " ",
150 | " tests[\"GetLoginTicket: failed\"] = false",
151 | " }",
152 | " else {",
153 | " ",
154 | " tests[\"GetLoginTicket: SUCCESS\"] = true",
155 | " }",
156 | "}"
157 | ],
158 | "type": "text/javascript"
159 | }
160 | }
161 | ],
162 | "request": {
163 | "method": "POST",
164 | "header": [
165 | {
166 | "key": "Content-Type",
167 | "value": "application/xml",
168 | "type": "text"
169 | }
170 | ],
171 | "body": {
172 | "mode": "raw",
173 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{_SESSION_TICKET}} \n \n \n \n \n \n \n\n"
174 | },
175 | "url": {
176 | "raw": "https://api.webex.com/WBXService/XMLService",
177 | "protocol": "https",
178 | "host": [
179 | "api",
180 | "webex",
181 | "com"
182 | ],
183 | "path": [
184 | "WBXService",
185 | "XMLService"
186 | ]
187 | }
188 | },
189 | "response": []
190 | },
191 | {
192 | "name": "GetloginurlUser",
193 | "event": [
194 | {
195 | "listen": "test",
196 | "script": {
197 | "id": "842a665a-8826-48e8-9dae-967d2c442e9f",
198 | "exec": [
199 | "if (responseCode.code != 200) {",
200 | " ",
201 | " tests[\"GetloginurlUser: failed\"] = false",
202 | "}",
203 | "else {",
204 | " ",
205 | " jsonData = xml2Json(responseBody)",
206 | " ",
207 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
208 | " ",
209 | " tests[\"GetloginurlUser: failed\"] = false",
210 | " }",
211 | " else {",
212 | " ",
213 | " tests[\"GetloginurlUser: SUCCESS\"] = true",
214 | " }",
215 | "}"
216 | ],
217 | "type": "text/javascript"
218 | }
219 | }
220 | ],
221 | "request": {
222 | "method": "POST",
223 | "header": [
224 | {
225 | "key": "Content-Type",
226 | "value": "application/xml",
227 | "type": "text"
228 | }
229 | ],
230 | "body": {
231 | "mode": "raw",
232 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{_SESSION_TICKET}} \n \n \n \n \n \tdstaudt\n \n \n\n"
233 | },
234 | "url": {
235 | "raw": "https://api.webex.com/WBXService/XMLService",
236 | "protocol": "https",
237 | "host": [
238 | "api",
239 | "webex",
240 | "com"
241 | ],
242 | "path": [
243 | "WBXService",
244 | "XMLService"
245 | ]
246 | }
247 | },
248 | "response": []
249 | },
250 | {
251 | "name": "AuthenticateUser (access token)",
252 | "event": [
253 | {
254 | "listen": "test",
255 | "script": {
256 | "id": "820bda34-3944-4a4a-a89d-8da62f149c47",
257 | "exec": [
258 | "if (responseCode.code != 200) {",
259 | " ",
260 | " tests[\"AuthenticateUser: failed\"] = false",
261 | "}",
262 | "else {",
263 | " ",
264 | " jsonData = xml2Json(responseBody)",
265 | " ",
266 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
267 | " ",
268 | " tests[\"AuthenticateUser: failed\"] = false",
269 | " }",
270 | " else {",
271 | " ",
272 | " tests[\"AuthenticateUser: SUCCESS\"] = true",
273 | " ",
274 | " pm.environment.set( \"_SESSION_TICKET\", jsonData['serv:message']['serv:body']['serv:bodyContent']['use:sessionTicket'] );",
275 | " }",
276 | "}"
277 | ],
278 | "type": "text/javascript"
279 | }
280 | }
281 | ],
282 | "request": {
283 | "method": "POST",
284 | "header": [
285 | {
286 | "key": "Content-Type",
287 | "value": "application/xml",
288 | "type": "text"
289 | }
290 | ],
291 | "body": {
292 | "mode": "raw",
293 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n \n \n \n \n \t{{ACCESS_TOKEN}}\n \n \n\n"
294 | },
295 | "url": {
296 | "raw": "https://api.webex.com/WBXService/XMLService",
297 | "protocol": "https",
298 | "host": [
299 | "api",
300 | "webex",
301 | "com"
302 | ],
303 | "path": [
304 | "WBXService",
305 | "XMLService"
306 | ]
307 | }
308 | },
309 | "response": []
310 | }
311 | ],
312 | "protocolProfileBehavior": {}
313 | },
314 | {
315 | "name": "General Session Service",
316 | "item": [
317 | {
318 | "name": "LstsummarySession",
319 | "event": [
320 | {
321 | "listen": "test",
322 | "script": {
323 | "id": "3de3d412-0e68-443b-bb44-931db47b7fda",
324 | "exec": [
325 | "if (responseCode.code != 200) {",
326 | " ",
327 | " tests[\"LstsummarySession: failed\"] = false",
328 | "}",
329 | "else {",
330 | " ",
331 | " jsonData = xml2Json(responseBody)",
332 | " ",
333 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
334 | " ",
335 | " tests[\"LstsummarySession: failed\"] = false",
336 | " }",
337 | " else {",
338 | " ",
339 | " tests[\"LstsummarySession: SUCCESS\"] = true",
340 | " }",
341 | "}"
342 | ],
343 | "type": "text/javascript"
344 | }
345 | }
346 | ],
347 | "request": {
348 | "method": "POST",
349 | "header": [
350 | {
351 | "key": "Content-Type",
352 | "value": "application/xml",
353 | "type": "text"
354 | }
355 | ],
356 | "body": {
357 | "mode": "raw",
358 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{_SESSION_TICKET}} \n \n \n \n \n \t{{WEBEXID}}\n \n \n\n"
359 | },
360 | "url": {
361 | "raw": "https://api.webex.com/WBXService/XMLService",
362 | "protocol": "https",
363 | "host": [
364 | "api",
365 | "webex",
366 | "com"
367 | ],
368 | "path": [
369 | "WBXService",
370 | "XMLService"
371 | ]
372 | }
373 | },
374 | "response": []
375 | }
376 | ],
377 | "protocolProfileBehavior": {}
378 | },
379 | {
380 | "name": "Meeting Type Service",
381 | "item": [
382 | {
383 | "name": "LstMeetingType",
384 | "event": [
385 | {
386 | "listen": "test",
387 | "script": {
388 | "id": "f412bbae-9a61-4245-b49d-bd71143dd555",
389 | "exec": [
390 | "if (responseCode.code != 200) {",
391 | " ",
392 | " tests[\"LstMeetingType: failed\"] = false",
393 | "}",
394 | "else {",
395 | " ",
396 | " jsonData = xml2Json(responseBody)",
397 | " ",
398 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
399 | " ",
400 | " tests[\"LstMeetingType: failed\"] = false",
401 | " }",
402 | " else {",
403 | " ",
404 | " tests[\"LstMeetingType: SUCCESS\"] = true",
405 | " }",
406 | "}"
407 | ],
408 | "type": "text/javascript"
409 | }
410 | }
411 | ],
412 | "request": {
413 | "method": "POST",
414 | "header": [
415 | {
416 | "key": "Content-Type",
417 | "value": "application/xml",
418 | "type": "text"
419 | }
420 | ],
421 | "body": {
422 | "mode": "raw",
423 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n \n \n\n"
424 | },
425 | "url": {
426 | "raw": "https://api.webex.com/WBXService/XMLService",
427 | "protocol": "https",
428 | "host": [
429 | "api",
430 | "webex",
431 | "com"
432 | ],
433 | "path": [
434 | "WBXService",
435 | "XMLService"
436 | ]
437 | }
438 | },
439 | "response": []
440 | }
441 | ],
442 | "protocolProfileBehavior": {}
443 | },
444 | {
445 | "name": "Meeting Service",
446 | "item": [
447 | {
448 | "name": "CreateMeeting",
449 | "event": [
450 | {
451 | "listen": "test",
452 | "script": {
453 | "id": "58342839-1891-4557-bcef-79b1a7869053",
454 | "exec": [
455 | "if (responseCode.code != 200) {",
456 | " ",
457 | " tests[\"CreateMeeting: failed\"] = false",
458 | "}",
459 | "else {",
460 | " ",
461 | " jsonData = xml2Json(responseBody)",
462 | " ",
463 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
464 | " ",
465 | " tests[\"CreateMeeting: failed\"] = false",
466 | " }",
467 | " else {",
468 | " ",
469 | " tests[\"CreateMeeting: SUCCESS\"] = true",
470 | " ",
471 | " pm.environment.set( \"_MEETING_KEY\", jsonData['serv:message']['serv:body']['serv:bodyContent']['meet:meetingkey'] );",
472 | " }",
473 | "}"
474 | ],
475 | "type": "text/javascript"
476 | }
477 | },
478 | {
479 | "listen": "prerequest",
480 | "script": {
481 | "id": "e5c9d9f2-40f0-42b0-82b4-7945f4d4fa84",
482 | "exec": [
483 | "var meetingTime = new Date();",
484 | "meetingTime.setMinutes( meetingTime.getMinutes() + 5 )",
485 | "",
486 | "var startDate = ( meetingTime.getMonth() + 1 ) + '/' +",
487 | " meetingTime.getDate() + '/' +",
488 | " meetingTime.getFullYear() +' ' +",
489 | " meetingTime.getHours() + ':' +",
490 | " meetingTime.getMinutes() + ':' +",
491 | " meetingTime.getSeconds()",
492 | "",
493 | "postman.setEnvironmentVariable(\"_MEETING_START_DATE\", startDate);",
494 | ""
495 | ],
496 | "type": "text/javascript"
497 | }
498 | }
499 | ],
500 | "request": {
501 | "method": "POST",
502 | "header": [
503 | {
504 | "key": "Content-Type",
505 | "value": "application/xml",
506 | "type": "text"
507 | }
508 | ],
509 | "body": {
510 | "mode": "raw",
511 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n \n Cisco1234\n \n \n Sample Meeting\n 105\n Test\n \n \n 4\n \n \n \n John Doe\n johndoe@apidemoeu.com\n \n \n \n \n \n true\n true\n true\n TRUE\n TRUE\n \n \n {{_MEETING_START_DATE}}\n 900\n true\n 20\n 4\n \n \n CALLIN\n \n Call 1-800-555-1234, Passcode 98765\n \n \n \n \n\n"
512 | },
513 | "url": {
514 | "raw": "https://api.webex.com/WBXService/XMLService",
515 | "protocol": "https",
516 | "host": [
517 | "api",
518 | "webex",
519 | "com"
520 | ],
521 | "path": [
522 | "WBXService",
523 | "XMLService"
524 | ]
525 | }
526 | },
527 | "response": []
528 | },
529 | {
530 | "name": "LstsummaryMeeting",
531 | "event": [
532 | {
533 | "listen": "test",
534 | "script": {
535 | "id": "3db3d0f9-9767-4db7-916a-1d2f9d8a18df",
536 | "exec": [
537 | "if (responseCode.code != 200) {",
538 | " ",
539 | " tests[\"LstsummaryMeeting: failed\"] = false",
540 | "}",
541 | "else {",
542 | " ",
543 | " jsonData = xml2Json(responseBody)",
544 | " ",
545 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
546 | " ",
547 | " tests[\"LstsummaryMeeting: failed\"] = false",
548 | " }",
549 | " else {",
550 | " ",
551 | " tests[\"LstsummaryMeeting: SUCCESS\"] = true",
552 | " }",
553 | "}"
554 | ],
555 | "type": "text/javascript"
556 | }
557 | }
558 | ],
559 | "request": {
560 | "method": "POST",
561 | "header": [
562 | {
563 | "key": "Content-Type",
564 | "value": "application/xml",
565 | "type": "text"
566 | }
567 | ],
568 | "body": {
569 | "mode": "raw",
570 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n \n 10\n AND\n \n \n STARTTIME\n ASC\n \n\t\t\t\n\t\t\t\t06/09/2019 15:51:00\n\t\t\t\n {{WEBEXID}}\n \n \n"
571 | },
572 | "url": {
573 | "raw": "https://api.webex.com/WBXService/XMLService",
574 | "protocol": "https",
575 | "host": [
576 | "api",
577 | "webex",
578 | "com"
579 | ],
580 | "path": [
581 | "WBXService",
582 | "XMLService"
583 | ]
584 | }
585 | },
586 | "response": []
587 | },
588 | {
589 | "name": "GetMeeting",
590 | "event": [
591 | {
592 | "listen": "test",
593 | "script": {
594 | "id": "9de34cfe-a266-4424-b76a-2b8c4c81faea",
595 | "exec": [
596 | "postman.setNextRequest('LstMeetingAttendee');",
597 | "",
598 | "if (responseCode.code != 200) {",
599 | " ",
600 | " tests[\"GetMeeting: failed\"] = false",
601 | "}",
602 | "else {",
603 | " ",
604 | " jsonData = xml2Json(responseBody)",
605 | " ",
606 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
607 | " ",
608 | " tests[\"GetMeeting: failed\"] = false",
609 | " }",
610 | " else {",
611 | " ",
612 | " tests[\"GetMeeting: SUCCESS\"] = true",
613 | " }",
614 | "}"
615 | ],
616 | "type": "text/javascript"
617 | }
618 | }
619 | ],
620 | "request": {
621 | "method": "POST",
622 | "header": [
623 | {
624 | "key": "Content-Type",
625 | "value": "application/xml",
626 | "type": "text"
627 | }
628 | ],
629 | "body": {
630 | "mode": "raw",
631 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n {{_MEETING_KEY}}\n \n \n"
632 | },
633 | "url": {
634 | "raw": "https://api.webex.com/WBXService/XMLService",
635 | "protocol": "https",
636 | "host": [
637 | "api",
638 | "webex",
639 | "com"
640 | ],
641 | "path": [
642 | "WBXService",
643 | "XMLService"
644 | ]
645 | }
646 | },
647 | "response": []
648 | },
649 | {
650 | "name": "DelMeeting",
651 | "event": [
652 | {
653 | "listen": "test",
654 | "script": {
655 | "id": "94958e7b-415f-4feb-84ca-33eb566d7a7a",
656 | "exec": [
657 | "postman.setNextRequest( null )",
658 | "",
659 | "if (responseCode.code != 200) {",
660 | " ",
661 | " tests[\"DelMeeting: failed\"] = false",
662 | "}",
663 | "else {",
664 | " ",
665 | " jsonData = xml2Json(responseBody)",
666 | " ",
667 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
668 | " ",
669 | " tests[\"DelMeeting: failed\"] = false",
670 | " }",
671 | " else {",
672 | " ",
673 | " tests[\"DelMeeting: SUCCESS\"] = true",
674 | " }",
675 | "}"
676 | ],
677 | "type": "text/javascript"
678 | }
679 | }
680 | ],
681 | "request": {
682 | "method": "POST",
683 | "header": [
684 | {
685 | "key": "Content-Type",
686 | "value": "application/xml",
687 | "type": "text"
688 | }
689 | ],
690 | "body": {
691 | "mode": "raw",
692 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n {{_MEETING_KEY}}\n \n \n"
693 | },
694 | "url": {
695 | "raw": "https://api.webex.com/WBXService/XMLService",
696 | "protocol": "https",
697 | "host": [
698 | "api",
699 | "webex",
700 | "com"
701 | ],
702 | "path": [
703 | "WBXService",
704 | "XMLService"
705 | ]
706 | }
707 | },
708 | "response": []
709 | },
710 | {
711 | "name": "GetjoinurlMeeting",
712 | "event": [
713 | {
714 | "listen": "test",
715 | "script": {
716 | "id": "9de34cfe-a266-4424-b76a-2b8c4c81faea",
717 | "exec": [
718 | "postman.setNextRequest('LstMeetingAttendee');",
719 | "",
720 | "if (responseCode.code != 200) {",
721 | " ",
722 | " tests[\"GetMeeting: failed\"] = false",
723 | "}",
724 | "else {",
725 | " ",
726 | " jsonData = xml2Json(responseBody)",
727 | " ",
728 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
729 | " ",
730 | " tests[\"GetMeeting: failed\"] = false",
731 | " }",
732 | " else {",
733 | " ",
734 | " tests[\"GetMeeting: SUCCESS\"] = true",
735 | " }",
736 | "}"
737 | ],
738 | "type": "text/javascript"
739 | }
740 | }
741 | ],
742 | "request": {
743 | "method": "POST",
744 | "header": [
745 | {
746 | "key": "Content-Type",
747 | "type": "text",
748 | "value": "application/xml"
749 | }
750 | ],
751 | "body": {
752 | "mode": "raw",
753 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n {{_MEETING_KEY}}\n \n \n"
754 | },
755 | "url": {
756 | "raw": "https://api.webex.com/WBXService/XMLService",
757 | "protocol": "https",
758 | "host": [
759 | "api",
760 | "webex",
761 | "com"
762 | ],
763 | "path": [
764 | "WBXService",
765 | "XMLService"
766 | ]
767 | }
768 | },
769 | "response": []
770 | }
771 | ],
772 | "protocolProfileBehavior": {}
773 | },
774 | {
775 | "name": "Meeting Attendee Service",
776 | "item": [
777 | {
778 | "name": "LstMeetingAttendee",
779 | "event": [
780 | {
781 | "listen": "test",
782 | "script": {
783 | "id": "533d520d-2649-4b46-8ed5-6087b164d6ba",
784 | "exec": [
785 | "postman.setNextRequest('DelMeeting');",
786 | "",
787 | "if (responseCode.code != 200) {",
788 | " ",
789 | " tests[\"LstMeetingAttendee: failed\"] = false",
790 | "}",
791 | "else {",
792 | " ",
793 | " jsonData = xml2Json(responseBody)",
794 | " ",
795 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
796 | " ",
797 | " tests[\"LstMeetingAttendee: failed\"] = false",
798 | " }",
799 | " else {",
800 | " ",
801 | " tests[\"LstMeetingAttendee: SUCCESS\"] = true",
802 | " }",
803 | "}"
804 | ],
805 | "type": "text/javascript"
806 | }
807 | }
808 | ],
809 | "request": {
810 | "method": "POST",
811 | "header": [
812 | {
813 | "key": "Content-Type",
814 | "type": "text",
815 | "value": "application/xml"
816 | }
817 | ],
818 | "body": {
819 | "mode": "raw",
820 | "raw": "\n\n \n\t\t\n\t\t\t{{SITE_NAME}}\n\t\t\t{{WEBEXID}}\n\t\t\t{{_SESSION_TICKET}} \n\t\t\n \n \n \n {{_MEETING_KEY}}\n \n \n\n"
821 | },
822 | "url": {
823 | "raw": "https://api.webex.com/WBXService/XMLService",
824 | "protocol": "https",
825 | "host": [
826 | "api",
827 | "webex",
828 | "com"
829 | ],
830 | "path": [
831 | "WBXService",
832 | "XMLService"
833 | ]
834 | }
835 | },
836 | "response": []
837 | }
838 | ],
839 | "protocolProfileBehavior": {}
840 | },
841 | {
842 | "name": "Site Service",
843 | "item": [
844 | {
845 | "name": "GetSite",
846 | "event": [
847 | {
848 | "listen": "test",
849 | "script": {
850 | "id": "820bda34-3944-4a4a-a89d-8da62f149c47",
851 | "exec": [
852 | "if (responseCode.code != 200) {",
853 | " ",
854 | " tests[\"AuthenticateUser: failed\"] = false",
855 | "}",
856 | "else {",
857 | " ",
858 | " jsonData = xml2Json(responseBody)",
859 | " ",
860 | " if ( jsonData['serv:message']['serv:header']['serv:response']['serv:result'] != 'SUCCESS') {",
861 | " ",
862 | " tests[\"AuthenticateUser: failed\"] = false",
863 | " }",
864 | " else {",
865 | " ",
866 | " tests[\"AuthenticateUser: SUCCESS\"] = true",
867 | " ",
868 | " pm.environment.set( \"_SESSION_TICKET\", jsonData['serv:message']['serv:body']['serv:bodyContent']['use:sessionTicket'] );",
869 | " }",
870 | "}"
871 | ],
872 | "type": "text/javascript"
873 | }
874 | }
875 | ],
876 | "request": {
877 | "method": "POST",
878 | "header": [
879 | {
880 | "key": "Content-Type",
881 | "value": "application/xml",
882 | "type": "text"
883 | }
884 | ],
885 | "body": {
886 | "mode": "raw",
887 | "raw": "\n\n \n \n {{SITE_NAME}}\n {{WEBEXID}}\n {{_SESSION_TICKET}} \n \n \n \n \n \n\n"
888 | },
889 | "url": {
890 | "raw": "https://api.webex.com/WBXService/XMLService",
891 | "protocol": "https",
892 | "host": [
893 | "api",
894 | "webex",
895 | "com"
896 | ],
897 | "path": [
898 | "WBXService",
899 | "XMLService"
900 | ]
901 | }
902 | },
903 | "response": []
904 | }
905 | ],
906 | "protocolProfileBehavior": {}
907 | }
908 | ],
909 | "event": [
910 | {
911 | "listen": "prerequest",
912 | "script": {
913 | "id": "3f7fe00d-0f00-4022-9ab0-c37f33d66a24",
914 | "type": "text/javascript",
915 | "exec": [
916 | ""
917 | ]
918 | }
919 | },
920 | {
921 | "listen": "test",
922 | "script": {
923 | "id": "2fd6d6ba-091e-4f0a-a4de-57af981c5f56",
924 | "type": "text/javascript",
925 | "exec": [
926 | ""
927 | ]
928 | }
929 | }
930 | ],
931 | "protocolProfileBehavior": {}
932 | }
--------------------------------------------------------------------------------
/oauth2.py:
--------------------------------------------------------------------------------
1 | # Webex Meetings OAuth2 sample, demonstrating how to authenticate a
2 | # Webex Meetings OAuth user using Python + Authlib, then use the
3 | # resulting access token to make a Meetings XML GetUser request
4 |
5 | # Note, this sample works with Webex OAuth enabled sites, and uses either:
6 |
7 | # * Webex Teams integration mechanism
8 | # * Webex Meetings integration mechanism
9 |
10 | # Configuration and setup:
11 |
12 | # 1. Rename .env.example to .env, and edit with your Webex site name/target
13 | # Webex ID (user@email.com)
14 |
15 | # 2. Register a Webex Teams OAuth integration per the steps at:
16 |
17 | # * Webex Teams integration mechanism: https://developer.webex.com/docs/integrations
18 | # * Webex Meetings integration mechanism: https://developer.cisco.com/docs/webex-meetings/#!integration
19 |
20 | # Set the Redirect URL to: https://127.0.0.1:5000/authorize
21 |
22 | # For Webex Teams integration, select the 'spark:all' scope
23 |
24 | # 3. Place the integration client_id and client_secret values into .env
25 |
26 | # 4. Generate the self-signed certificate used to serve the Flask web app with HTTPS.
27 |
28 | # This requires that OpenSSL tools are installed (the command below was used on Ubuntu Linux 19.04.)
29 |
30 | # From a terminal at the repo root:
31 |
32 | # openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
33 |
34 | # Launching the app with Visual Studio Code:
35 |
36 | # 1. Open the repo root folder with VS Code
37 |
38 | # 2. Open the command palette (View/Command Palette), and find 'Python: select interpreter'
39 |
40 | # Select the Python3 interpreter desired (e.g. a 'venv' environment)
41 |
42 | # 3. From the Debug pane, select the launch option 'Python: Launch oauth2.py'
43 |
44 | # 4. Open a browser and navigate to: https://127.0.0.1:5000
45 |
46 | # The application will start the OAuth2 flow, then redirect to the /GetUser URL to display the
47 | # target user's Webex Meetings API details in XML format
48 |
49 | # Copyright (c) 2019 Cisco and/or its affiliates.
50 | # Permission is hereby granted, free of charge, to any person obtaining a copy
51 | # of this software and associated documentation files (the "Software"), to deal
52 | # in the Software without restriction, including without limitation the rights
53 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
54 | # copies of the Software, and to permit persons to whom the Software is
55 | # furnished to do so, subject to the following conditions:
56 | # The above copyright notice and this permission notice shall be included in all
57 | # copies or substantial portions of the Software.
58 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
61 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
62 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
63 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
64 | # SOFTWARE.
65 |
66 | from flask import Flask, url_for, redirect, session, make_response, request
67 | from authlib.integrations.flask_client import OAuth
68 | from lxml import etree
69 | import requests
70 | from furl import furl
71 | import json
72 | import os
73 |
74 | # Edit .env file to specify your Webex integration client ID / secret
75 | from dotenv import load_dotenv
76 | load_dotenv( override=True ) # Prefer variables in .env file
77 |
78 | # Enable Authlib and API request/response debug output in .env
79 | DEBUG = os.getenv('DEBUG_ENABLED') == 'True'
80 |
81 | # Instantiate the Flask application
82 | app = Flask(__name__)
83 |
84 | # This key is used to encrypt the Flask user session data store,
85 | # you might want to make this more secret
86 | app.secret_key = "CHANGEME"
87 |
88 | # Create an Authlib registry object to handle OAuth2 authentication
89 | oauth = OAuth(app)
90 |
91 | # This simple function returns the authentication token object stored in the Flask
92 | # user session data store
93 | # It is used by the webex RemoteApp object below to retrieve the access token when
94 | # making API requests on the session user's behalf
95 | def fetch_token():
96 |
97 | return session[ 'token' ]
98 |
99 | # Webex returns no 'token_type' in its /authorize response.
100 | # This authlib compliance fix adds its as 'bearer'
101 | def webex_compliance_fix( session ):
102 |
103 | def _fix( resp ):
104 |
105 | token = resp.json()
106 |
107 | token[ 'token_type' ] = 'bearer'
108 |
109 | resp._content = json.dumps( token ).encode( 'utf-8' )
110 |
111 | return resp
112 |
113 | session.register_compliance_hook('access_token_response', _fix)
114 |
115 | # Register a RemoteApplication for the Webex Meetings XML API and OAuth2 service
116 | # The code_challenge_method will cause it to use the PKCE mechanism with SHA256
117 | # The client_kwargs sets the requested Webex Meetings integration scope
118 | # and the token_endpoint_auth_method to use when exchanging the auth code for the
119 | # access token
120 |
121 | if ( os.getenv( 'OAUTH_TYPE') == 'MEETINGS' ):
122 | authUrl = 'https://api.webex.com/v1/oauth2/authorize'
123 | tokenUrl = 'https://api.webex.com/v1/oauth2/token'
124 | refreshUrl = 'https://api.ciscospark.com/v1/authorize'
125 | scopes = 'all_read+user_modify+meeting_modify+recording_modify+setting_modify'
126 | else:
127 | authUrl = 'https://api.ciscospark.com/v1/authorize'
128 | tokenUrl = 'https://api.ciscospark.com/v1/access_token'
129 | refreshUrl = 'https://api.webex.com/v1/oauth2/token'
130 | scopes = 'spark:all'
131 |
132 | if DEBUG:
133 | import logging
134 | import sys
135 | log = logging.getLogger('authlib')
136 | log.addHandler(logging.StreamHandler(sys.stdout))
137 | log.setLevel(logging.DEBUG)
138 |
139 | oauth.register(
140 |
141 | name = 'webex',
142 | client_id = os.getenv( 'CLIENT_ID' ),
143 | client_secret = os.getenv( 'CLIENT_SECRET' ),
144 | authorize_url = authUrl,
145 | access_token_url = tokenUrl,
146 | refresh_token_url = refreshUrl,
147 | api_base_url = 'https://api.webex.com/WBXService/XMLService',
148 | client_kwargs = {
149 | 'scope': scopes,
150 | 'token_endpoint_auth_method': 'client_secret_post'
151 | },
152 | code_challenge_method = 'S256',
153 | fetch_token = fetch_token,
154 | compliance_fix=webex_compliance_fix
155 | )
156 |
157 | # The following section handles the Webex Meetings XML API calls
158 |
159 | # Custom exception for errors when sending Meetings API requests
160 | class SendRequestError(Exception):
161 |
162 | def __init__(self, result, reason):
163 | self.result = result
164 | self.reason = reason
165 |
166 | pass
167 |
168 | # Generic function for sending Meetings XML API requests
169 | # envelope : the full XML content of the request
170 | # debug : (optional) print the XML of the request / response
171 | def sendRequest( envelope, debug = False ):
172 |
173 | # Use the webex_meetings RemoteApplication object to POST the XML envelope to the Meetings API endpoint
174 | # Note, this object is based on the Python requests library object and can accept similar kwargs
175 | headers = { 'Content-Type': 'application/xml'}
176 | response = oauth.webex.post( url = '', data = envelope, headers = headers )
177 |
178 | if DEBUG:
179 | print( response.request.headers )
180 | print( response.request.body )
181 |
182 | # Check for HTTP errors, if we got something besides a 200 OK
183 | try:
184 | response.raise_for_status()
185 | except requests.exceptions.HTTPError as err:
186 | raise SendRequestError( 'HTTP ' + str(response.status_code), response.content.decode("utf-8") )
187 |
188 | # Use the lxml ElementTree object to parse the response XML
189 | message = etree.fromstring( response.content )
190 |
191 | # If debug mode has been requested, pretty print the XML to console
192 | if DEBUG:
193 | print( response.headers )
194 | print( etree.tostring( message, pretty_print = True, encoding = 'unicode' ) )
195 |
196 | # Use the find() method with an XPath to get the element's text
197 | # Note: {*} is pre-pended to each element name - matches any namespace.
198 | # If not SUCCESS...
199 | if message.find( '{*}header/{*}response/{*}result').text != 'SUCCESS':
200 |
201 | result = message.find( '{*}header/{*}response/{*}result').text
202 | reason = message.find( '{*}header/{*}response/{*}reason').text
203 |
204 | #...raise an exception containing the result and reason element content
205 | raise SendRequestError( result, reason )
206 |
207 | # Return the XML message
208 | return message
209 |
210 | def WebexAuthenticateUser( siteName, webExId, accessToken ):
211 |
212 | # Use f-string literal formatting to substitute {variables} into the XML string
213 | request = f'''
214 |
216 |
217 |
218 | {siteName}
219 | {webExId}
220 |
221 |
222 |
223 |
224 | {accessToken}
225 |
226 |
227 | '''
228 |
229 | # Make the API request
230 | response = sendRequest( request )
231 |
232 | # Return an object containing the security context info with sessionTicket
233 | return response.find( '{*}body/{*}bodyContent/{*}sessionTicket' ).text
234 |
235 | def WebexGetUser( sessionSecurityContext, webExId ):
236 |
237 | # Use f-string literal formatting to substitute {variables} into the XML template string
238 | request = f'''
239 |
241 |
242 | { sessionSecurityContext }
243 |
244 |
245 |
246 | {webExId}
247 |
248 |
249 |
250 | '''
251 |
252 | # Make the API request
253 | response = sendRequest( request, debug = True )
254 |
255 | # Return an object containing the security context info with sessionTicket
256 | return response
257 |
258 | # The Flask web app routes start below
259 |
260 | # This is the entry point of the app - navigate to https://localhost:5000 to start
261 | @app.route('/')
262 | def login():
263 |
264 | # Create the URL pointing to the web app's /authorize endpoint
265 | redirect_uri = url_for( 'authorize', _external=True)
266 |
267 | # Use the URL as the destination to receive the auth code, and
268 | # kick-off the Authclient OAuth2 login flow/GetUser
269 | return oauth.webex.authorize_redirect( redirect_uri )
270 |
271 | # This URL handles receiving the auth code after the OAuth2 flow is complete
272 | @app.route('/authorize')
273 | def authorize():
274 |
275 | # Go ahead and exchange the auth code for the access token
276 | # and store it in the Flask user session object
277 | try:
278 | session[ 'token' ] = oauth.webex.authorize_access_token()
279 |
280 | except Exception as err:
281 |
282 | response = 'Error exchanging auth code for access token:
'
283 | response += f'- Error: HTTP { err.code } { err.name }
'
284 | response += f'- Description: { err.description }
'
285 |
286 | return response, 500
287 |
288 | # Now that we have the API access token, redirect the the URL for making a
289 | # Webex Meetings API GetUser request
290 | return redirect( url_for( 'GetUser' ), code = '302' )
291 |
292 | # Make a Meetings API GetUser request and return the raw XML to the browser
293 | @app.route('/GetUser')
294 | def GetUser():
295 |
296 | if ( os.getenv( 'OAUTH_TYPE' ) == 'MEETINGS' ):
297 | sessionSecurityContext = f'''
298 |
299 | { os.getenv( 'SITENAME' ) }
300 | { os.getenv( 'WEBEXID' ) }
301 | { session[ 'token' ][ 'access_token' ] }
302 | '''
303 |
304 | else:
305 | # Call AuthenticateUser to transform the Webex Teams access token into a
306 | # Webex Meetings session ticket
307 | try:
308 | sessionTicket = WebexAuthenticateUser(
309 | os.getenv( 'SITENAME' ),
310 | os.getenv( 'WEBEXID' ),
311 | session[ 'token' ][ 'access_token' ]
312 | )
313 |
314 | sessionSecurityContext = f'''
315 |
316 | { os.getenv( 'SITENAME' ) }
317 | { os.getenv( 'WEBEXID' ) }
318 | { sessionTicket }
319 | '''
320 |
321 | except SendRequestError as err:
322 |
323 | response = 'Error making AuthenticateUser request:
'
324 | response += '- Result: ' + err.result + '
'
325 | response += '- Reason: ' + err.reason + '
'
326 |
327 | return response, 500
328 |
329 | # Call the function we created above, grabbing siteName and webExId from .env, and the
330 | # access_token from the token object in the session store
331 | try:
332 |
333 | reply = WebexGetUser(
334 | sessionSecurityContext,
335 | os.getenv( 'WEBEXID' )
336 | )
337 |
338 | except SendRequestError as err:
339 |
340 | response = 'Error making Webex Meeting API request:
'
341 | response += '- Result: ' + err.result + '
'
342 | response += '- Reason: ' + err.reason + '
'
343 |
344 | return response, 500
345 |
346 | # Create a Flask Response object, with content of the pretty-printed XML text
347 | response = make_response( etree.tostring( reply, pretty_print = True, encoding = 'unicode' ) )
348 |
349 | # Mark the response as XML via the Content-Type header
350 | response.headers[ 'Content-Type' ] = 'application/xml'
351 |
352 | return response
353 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Authlib==0.15.2
2 | certifi==2020.12.5
3 | cffi==1.14.4
4 | chardet==4.0.0
5 | click==7.1.2
6 | cryptography==3.3.1
7 | Flask==1.1.2
8 | furl==2.1.0
9 | idna==2.10
10 | itsdangerous==1.1.0
11 | Jinja2==2.11.2
12 | lxml==4.6.2
13 | MarkupSafe==1.1.1
14 | orderedmultidict==1.0.1
15 | pycparser==2.20
16 | python-dotenv==0.15.0
17 | requests==2.25.1
18 | six==1.15.0
19 | urllib3==1.26.2
20 | Werkzeug==1.0.1
21 |
--------------------------------------------------------------------------------
/sampleFlow.py:
--------------------------------------------------------------------------------
1 | # Webex Meetings XML API sample script, demonstrating the following work flow:
2 |
3 | # AuthenticateUser
4 | # GetUser
5 | # CreateMeeting
6 | # LstsummaryMeeting
7 | # GetMeeting
8 | # DelMeeting
9 |
10 | # Configuration and setup:
11 |
12 | # Configuration and setup:
13 |
14 | # * Edit .env to provide your Webex user credentials
15 |
16 | # - For Control Hub managed sites with SSO enabled, provide ACCESS_TOKEN as a
17 | # Webex Teams token (retrieve by logging in here:
18 | # https://developer.webex.com/docs/api/getting-started )
19 | # - For non-SSO-enabled sites, provide the PASSWORD
20 |
21 | # If both are provided, the sample will attempt to use the ACCESS_TOKEN
22 |
23 | # Copyright (c) 2019 Cisco and/or its affiliates.
24 | # Permission is hereby granted, free of charge, to any person obtaining a copy
25 | # of this software and associated documentation files (the "Software"), to deal
26 | # in the Software without restriction, including without limitation the rights
27 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28 | # copies of the Software, and to permit persons to whom the Software is
29 | # furnished to do so, subject to the following conditions:
30 | # The above copyright notice and this permission notice shall be included in all
31 | # copies or substantial portions of the Software.
32 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38 | # SOFTWARE.
39 |
40 | import requests
41 | import datetime
42 | from lxml import etree
43 | import os
44 |
45 | # Edit .env file to specify your Webex site/user details
46 | from dotenv import load_dotenv
47 | load_dotenv( override=True ) # Prefer variables in .env file
48 |
49 | # Enable Authlib and API request/response debug output in .env
50 | DEBUG = os.getenv('DEBUG_ENABLED') == 'True'
51 |
52 | # Once the user is authenticated, the sessionTicket for all API requests will be stored here
53 | sessionSecurityContext = { }
54 |
55 | # Custom exception for errors when sending requests
56 | class SendRequestError(Exception):
57 |
58 | def __init__(self, result, reason):
59 | self.result = result
60 | self.reason = reason
61 |
62 | pass
63 |
64 | # Generic function for sending XML API requests
65 | # envelope : the full XML content of the request
66 | def sendRequest( envelope ):
67 |
68 | if DEBUG:
69 | print( envelope )
70 |
71 | # Use the requests library to POST the XML envelope to the Webex API endpoint
72 | response = requests.post( 'https://api.webex.com/WBXService/XMLService', envelope )
73 |
74 | # Check for HTTP errors
75 | try:
76 | response.raise_for_status()
77 | except requests.exceptions.HTTPError:
78 | raise SendRequestError( 'HTTP ' + str(response.status_code), response.content.decode("utf-8") )
79 |
80 | # Use the lxml ElementTree object to parse the response XML
81 | message = etree.fromstring( response.content )
82 |
83 | if DEBUG:
84 | print( etree.tostring( message, pretty_print = True, encoding = 'unicode' ) )
85 |
86 | # Use the find() method with an XPath to get the 'result' element's text
87 | # Note: {*} is pre-pended to each element name - ignores namespaces
88 | # If not SUCCESS...
89 | if message.find( '{*}header/{*}response/{*}result').text != 'SUCCESS':
90 |
91 | result = message.find( '{*}header/{*}response/{*}result').text
92 | reason = message.find( '{*}header/{*}response/{*}reason').text
93 |
94 | #...raise an exception containing the result and reason element content
95 | raise SendRequestError( result, reason )
96 |
97 | return message
98 |
99 | def AuthenticateUser( siteName, webExId, password, accessToken ):
100 |
101 | # If an access token is provided in .env, we'll use this form
102 | if ( accessToken ):
103 | request = f'''
104 |
106 |
107 |
108 | {siteName}
109 | {webExId}
110 |
111 |
112 |
113 |
114 | {accessToken}
115 |
116 |
117 | '''
118 | else:
119 | # If no access token, assume a password was provided, using this form
120 | request = f'''
121 |
123 |
124 |
125 | {siteName}
126 | {webExId}
127 | {password}
128 |
129 |
130 |
131 |
132 |
133 | '''
134 |
135 | # Make the API request
136 | response = sendRequest( request )
137 |
138 | # Return an object containing the security context info with sessionTicket
139 | return {
140 | 'siteName': siteName,
141 | 'webExId': webExId,
142 | 'sessionTicket': response.find( '{*}body/{*}bodyContent/{*}sessionTicket' ).text
143 | }
144 |
145 | def GetUser( sessionSecurityContext ):
146 |
147 | request = f'''
148 |
150 |
151 |
152 | {sessionSecurityContext[ 'siteName' ]}
153 | {sessionSecurityContext[ 'webExId' ]}
154 | {sessionSecurityContext[ 'sessionTicket' ]}
155 |
156 |
157 |
158 |
159 | {sessionSecurityContext[ 'webExId' ]}
160 |
161 |
162 | '''
163 |
164 | # Make the API request
165 | response = sendRequest( request )
166 |
167 | # Return an object containing the response
168 | return response
169 |
170 | def CreateMeeting( sessionSecurityContext,
171 | meetingPassword,
172 | confName,
173 | meetingType,
174 | agenda,
175 | startDate ):
176 |
177 | request = f'''
178 |
180 |
181 |
182 | {sessionSecurityContext["siteName"]}
183 | {sessionSecurityContext["webExId"]}
184 | {sessionSecurityContext["sessionTicket"]}
185 |
186 |
187 |
188 |
190 |
191 | {meetingPassword}
192 |
193 |
194 | {confName}
195 | {meetingType}
196 | {agenda}
197 |
198 |
199 | true
200 | true
201 | true
202 | TRUE
203 | TRUE
204 |
205 |
206 | {startDate}
207 | 900
208 | false
209 | 20
210 | 4
211 |
212 |
213 | CALLIN
214 |
215 | Call 1-800-555-1234, Passcode 98765
216 |
217 |
218 |
219 |
220 | '''
221 |
222 | response = sendRequest( request )
223 |
224 | return response
225 |
226 | def LstsummaryMeeting( sessionSecurityContext,
227 | maximumNum,
228 | orderBy,
229 | orderAD,
230 | hostWebExId,
231 | startDateStart ):
232 |
233 | request = f'''
234 |
236 |
237 |
238 | {sessionSecurityContext["siteName"]}
239 | {sessionSecurityContext["webExId"]}
240 | {sessionSecurityContext["sessionTicket"]}
241 |
242 |
243 |
244 |
245 |
246 | {maximumNum}
247 | AND
248 |
249 |
250 | {orderBy}
251 | {orderAD}
252 |
253 |
254 | {startDateStart}
255 | 4
256 |
257 | {hostWebExId}
258 |
259 |
260 | '''
261 |
262 | response = sendRequest( request )
263 |
264 | return response
265 |
266 | def GetMeeting( sessionSecurityContext, meetingKey ):
267 |
268 | request = f'''
269 |
272 |
273 |
274 | {sessionSecurityContext["siteName"]}
275 | {sessionSecurityContext["webExId"]}
276 | {sessionSecurityContext["sessionTicket"]}
277 |
278 |
279 |
280 |
281 | {meetingKey}
282 |
283 |
284 | '''
285 |
286 | response = sendRequest( request )
287 |
288 | return response
289 |
290 | def DelMeeting( sessionSecurityContext, meetingKey ):
291 |
292 | request = f'''
293 |
296 |
297 |
298 | {sessionSecurityContext["siteName"]}
299 | {sessionSecurityContext["webExId"]}
300 | {sessionSecurityContext["sessionTicket"]}
301 |
302 |
303 |
304 |
305 | {meetingKey}
306 |
307 |
308 | '''
309 |
310 | response = sendRequest( request )
311 |
312 | return response
313 |
314 | if __name__ == "__main__":
315 |
316 | # AuthenticateUser and get sesssionTicket
317 | try:
318 | sessionSecurityContext = AuthenticateUser(
319 | os.getenv( 'SITENAME'),
320 | os.getenv( 'WEBEXID'),
321 | os.getenv( 'PASSWORD'),
322 | os.getenv( 'ACCESS_TOKEN' )
323 | )
324 |
325 | # If an error occurs, print the error details and exit the script
326 | except SendRequestError as err:
327 | print(err)
328 | raise SystemExit
329 |
330 | print( )
331 | print( 'Session Ticket:', '\n' )
332 | print( sessionSecurityContext[ 'sessionTicket' ] )
333 | print( )
334 |
335 | # Wait for the uesr to press Enter
336 | input( 'Press Enter to continue...' )
337 |
338 | # GetSite - this will allow us to determine which meeting types are
339 | # supported by the user's site. Then we'll parse/save the first type.
340 |
341 | try:
342 | response = GetUser( sessionSecurityContext )
343 |
344 | except SendRequestError as err:
345 | print(err)
346 | raise SystemExit
347 |
348 | meetingType = response.find( '{*}body/{*}bodyContent/{*}meetingTypes')[ 0 ].text
349 |
350 | print( )
351 | print( f'First meetingType available: {meetingType}' )
352 | print( )
353 |
354 | input( 'Press Enter to continue...' )
355 |
356 | # CreateMeeting - some options will be left out, some hard-coded in the XML
357 | # and some can be specified with variables
358 |
359 | # Use the datetime package to create a variable for the meeting time, 'now' plus 300 sec
360 | timestamp = datetime.datetime.now() + datetime.timedelta( seconds = 300 )
361 |
362 | # Create a string variable with the timestamp in the specific format required by the API
363 | strDate = timestamp.strftime( '%m/%d/%Y %H:%M:%S' )
364 |
365 | try:
366 | response = CreateMeeting( sessionSecurityContext,
367 | meetingPassword = 'C!sco123',
368 | confName = 'Test Meeting',
369 | meetingType = meetingType,
370 | agenda = 'Test meeting creation',
371 | startDate = strDate )
372 |
373 | except SendRequestError as err:
374 | print(err)
375 | raise SystemExit
376 |
377 | print( )
378 | print( 'Meeting Created:', '\n')
379 | print( ' Meeting Key:', response.find( '{*}body/{*}bodyContent/{*}meetingkey').text )
380 | print( )
381 |
382 | input( 'Press Enter to continue...' )
383 |
384 | # LstsummaryMeeting for all upcoming meetings for the user
385 | try:
386 | response = LstsummaryMeeting( sessionSecurityContext,
387 | maximumNum = 10,
388 | orderBy = 'STARTTIME',
389 | orderAD = 'ASC',
390 | hostWebExId = os.getenv('WEBEXID'),
391 | startDateStart = datetime.datetime.now().strftime('%m/%d/%Y %H:%M:%S') )
392 |
393 | except SendRequestError as err:
394 | print(err)
395 | raise SystemExit
396 |
397 | print( )
398 | print( 'Upcoming Meetings:', '\n')
399 |
400 | print( '{0:22}{1:25}{2:25}'.format( 'Start Time', 'Meeting Name', 'Meeting Key' ) )
401 | print( '{0:22}{1:25}{2:25}'.format( '-' * 10, '-' * 12, '-' * 11 ) )
402 |
403 | nextMeetingKey = response.find( '{*}body/{*}bodyContent/{*}meeting/{*}meetingKey').text
404 |
405 | for meeting in response.iter( '{*}meeting' ):
406 |
407 | print( '{0:22}{1:25}{2:25}'.format( meeting.find( '{*}startDate' ).text,
408 | meeting.find( '{*}confName' ).text,
409 | meeting.find( '{*}meetingKey' ).text ) )
410 |
411 | print( )
412 | input( 'Press Enter to continue...' )
413 |
414 | try:
415 | response = GetMeeting( sessionSecurityContext, nextMeetingKey )
416 | except SendRequestError as err:
417 | print(err)
418 | raise SystemError
419 |
420 | print( )
421 | print( 'Next Meeting Details:', '\n')
422 | print( ' Meeting Name:', response.find( '{*}body/{*}bodyContent/{*}metaData/{*}confName').text )
423 | print( ' Meeting Key:', response.find( '{*}body/{*}bodyContent/{*}meetingkey').text )
424 | print( ' Start Time:', response.find( '{*}body/{*}bodyContent/{*}schedule/{*}startDate').text )
425 | print( ' Join Link:', response.find( '{*}body/{*}bodyContent/{*}meetingLink').text )
426 | print( ' Password:', response.find( '{*}body/{*}bodyContent/{*}accessControl/{*}meetingPassword').text )
427 |
428 | print( )
429 | input( 'Press Enter to continue...' )
430 |
431 | try:
432 | response = DelMeeting( sessionSecurityContext, nextMeetingKey )
433 | except SendRequestError as err:
434 | print(err)
435 | raise SystemError
436 |
437 | print( )
438 | print( 'Next Meeting Delete: SUCCESS', '\n')
439 |
--------------------------------------------------------------------------------