├── .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 | --------------------------------------------------------------------------------