From b442d39c188288d3d33c269a2181c5afc748adc7 Mon Sep 17 00:00:00 2001 From: Matthias Guillitte Date: Mon, 17 Aug 2026 14:21:02 +0200 Subject: [PATCH] Webex first draft --- cloud/APIs/webex.py | 111 ++++++++++++++++++++++++++++++++++++++++++++ cloud/README.md | 6 ++- cloud/app.py | 86 +++++++++++++++++++++++++++++++--- 3 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 cloud/APIs/webex.py diff --git a/cloud/APIs/webex.py b/cloud/APIs/webex.py new file mode 100644 index 0000000..f92a620 --- /dev/null +++ b/cloud/APIs/webex.py @@ -0,0 +1,111 @@ + +class WebexManager: + def __init__(self, db_collection, client_id, client_secret, redirect_uri, team_id, user_id): + self.db = db_collection + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.team_id = team_id + self.user_id = user_id + self.base_url = "https://webexapis.com/v1" + + def save_tokens(self, token_data: dict): + """Saves or updates Webex OAuth tokens in MongoDB.""" + now = time.time() + doc = { + "_id": "webex_credentials", + "access_token": token_data.get("access_token"), + "refresh_token": token_data.get("refresh_token"), + "expires_at": now + token_data.get("expires_in", 43200) - 300, # Subtract 5 mins buffer + "refresh_token_expires_at": now + token_data.get("refresh_token_expires_in", 7776000), + "updated_at": datetime.datetime.now(datetime.timezone.utc).isoformat() + } + self.db.replace_one({"_id": "webex_credentials"}, doc, upsert=True) + return doc + + def get_access_token(self) -> str: + """Retrieves a valid access token from MongoDB, auto-refreshing if expired.""" + token_doc = self.db.find_one({"_id": "webex_credentials"}) + if not token_doc: + raise Exception("No Webex OAuth tokens found. Please complete login at /oauth/login.") + + # Check if access token is expired + if time.time() >= token_doc.get("expires_at", 0): + current_app.logger.info("Webex access token expired. Refreshing...") + return self.refresh_token(token_doc.get("refresh_token")) + + return token_doc.get("access_token") + + def exchange_code(self, code: str) -> dict: + """Exchanges OAuth code for access and refresh tokens.""" + payload = { + "grant_type": "authorization_code", + "client_id": self.client_id, + "client_secret": self.client_secret, + "code": code, + "redirect_uri": self.redirect_uri + } + res = requests.post(f"{self.base_url}/access_token", data=payload) + res.raise_for_status() + return self.save_tokens(res.json()) + + def refresh_token(self, refresh_token: str) -> str: + """Refreshes the Webex access token using the stored refresh_token.""" + payload = { + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + "refresh_token": refresh_token + } + res = requests.post(f"{self.base_url}/access_token", data=payload) + res.raise_for_status() + new_doc = self.save_tokens(res.json()) + return new_doc["access_token"] + + def create_support_room(self, client_name: str, client_email: str) -> str: + """Creates a team support room and invites the client and yourself.""" + access_token = self.get_access_token() + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + # 1. Create room under team + room_res = requests.post( + f"{self.base_url}/rooms", + headers=headers, + json={ + "title": f"Smartwave Support: {client_name}", + "teamId": self.team_id + } + ) + room_res.raise_for_status() + room_id = room_res.json()["id"] + + # 2. Add Client via email (Webex sends invite if user does not exist) + if client_email: + requests.post( + f"{self.base_url}/memberships", + headers=headers, + json={"roomId": room_id, "personEmail": client_email} + ) + + # 3. Add yourself (WEBEX_NINLUC_ID) + if self.user_id: + requests.post( + f"{self.base_url}/memberships", + headers=headers, + json={"roomId": room_id, "personId": self.user_id} + ) + + # 4. Optional: Send initial welcome message in the room + requests.post( + f"{self.base_url}/messages", + headers=headers, + json={ + "roomId": room_id, + "text": f"Support room initialized for {client_name}. An agent will be with you shortly." + } + ) + + return room_id \ No newline at end of file diff --git a/cloud/README.md b/cloud/README.md index 73fc519..7c497e8 100644 --- a/cloud/README.md +++ b/cloud/README.md @@ -1,4 +1,8 @@ in `/cloud` folder : -`flask run --debug` \ No newline at end of file +`flask run --debug` + +# Oauth Webex + +[webexapis.com](https://webexapis.com/v1/authorize?client_id=C2a4f544bb4f65503e60b6dceded0de194323a873360b2d06eff5a2232ff46176&response_type=code&redirect_uri=https%3A%2F%2Fsmartwave.matthiasg.dev%2Foauth%2Fcallback&scope=spark-admin%3Abroadworks_subscribers_write%20meeting%3Aadmin_participants_read%20spark%3Awebhooks_read%20spark%3Apeople_write%20spark-admin%3Awholesale_customers_write%20meeting%3Asummaries_write%20spark-admin%3Aworkspace_metrics_read%20cjp%3Atask_read%20spark-admin%3Awholesale_billing_reports_read%20meeting%3Asummaries_read%20spark-admin%3Atelephony_pstn_write%20spark-admin%3Avideo_mesh_api_webhook_write%20spark%3Aplaces_read%20cjp%3Aconfig_write%20meeting%3Aadmin_config_write%20spark-admin%3Arecordings_read%20spark-compliance%3Amessages_read%20cjds%3Aadmin_org_read%20spark-admin%3Aworkspaces_write%20spark-compliance%3Ameetings_write%20spark-admin%3Alocations_write%20meeting%3Aadmin_schedule_write%20identity%3Aplaceonetimepassword_create%20spark-admin%3Aorganizations_write%20spark-admin%3Atelephony_pstn_read%20spark%3Adevices_write%20spark-admin%3Awholesale_sub_partners_write%20cjp%3Aconfig_read%20spark-admin%3Abroadworks_billing_reports_write%20spark-admin%3Acall_qualities_read%20spark%3Aapplications_token%20spark%3Akms%20spark-admin%3Awholesale_sub_partners_read%20spark%3Awebrtc_calling%20spark-admin%3Amessages_read%20application%3Awebhooks_write%20spark-admin%3Awholesale_subscribers_read%20spark-admin%3Acalls_write%20meeting%3Aadmin_config_read%20spark%3Atelephony_config_write%20meeting%3Aparticipants_read%20Identity%3Acontact%20spark-admin%3Awholesale_billing_reports_write%20spark-admin%3Avideo_mesh_api_read%20Identity%3AConfig%20spark-compliance%3Awebhooks_write%20spark-admin%3Aorganizations_read%20cjds%3Aadmin_org_write%20spark-compliance%3Ateam_memberships_read%20meeting%3Aschedules_write%20spark-admin%3Avideo_mesh_api_write%20spark-admin%3Arecordings_write%20identity%3Aorganizations_rw%20identity%3Aorganizations_read%20cloud-contact-center%3Apod_conv%20spark-admin%3Adevices_read%20cjp%3Auser%20cjp%3Aconfig%20spark-admin%3Atelephony_config_write%20spark%3Arecordings_write%20spark-admin%3Abroadworks_enterprises_read%20spark%3Acalls_read%20meeting%3Arecordings_write%20spark%3Adevices_read%20spark-compliance%3Aevents_read%20spark-admin%3Aresource_group_memberships_read%20identity%3Apeople_rw%20spark-admin%3Aresource_group_memberships_write%20application%3Awebhooks_read%20spark-admin%3Ahybrid_connectors_read%20spark-compliance%3Ateams_read%20spark-admin%3Aplaces_write%20spark%3Amcp%20spark-admin%3Alicenses_read%20spark%3Aplaces_write%20webexsquare%3Aadmin%20spark%3Awebhooks_write%20meeting%3Aadmin_preferences_write%20spark%3Aall%20meeting%3Aadmin_preferences_read%20analytics%3Aread_all%20spark-admin%3Apeople_write%20spark%3Aorganizations_read%20spark-admin%3Aplaces_read%20spark-admin%3Awholesale_workspace_write%20spark-compliance%3Ateam_memberships_write%20identity%3Agroups_read%20identity%3Atokens_read%20wxc-dedicateduc%3Aadmin_perfmon_read%20spark-compliance%3Arecordings_read%20spark-admin%3Adevices_write%20spark%3Acalls_write%20Identity%3Aone_time_password%20spark-admin%3Aworkspace_locations_read%20spark-compliance%3Arecordings_write%20spark%3Axapi_commands%20spark-compliance%3Awebhooks_read%20spark-compliance%3Amessages_write%20spark-admin%3Awholesale_customers_read%20spark%3Axsi%20spark-admin%3Areports_read%20meeting%3Aparticipants_write%20spark-admin%3Apeople_read%20meeting%3Aadmin_transcripts_read%20spark-compliance%3Amemberships_read%20spark-admin%3Aresource_groups_read%20meeting%3Arecordings_read%20spark-admin%3Alocations_read%20meeting%3Apreferences_write%20meeting%3Aadmin_recordings_read%20identity%3Atokens_write%20meeting%3Atranscripts_read%20spark-admin%3Amessages_write%20spark%3Axapi_statuses%20spark-admin%3Acalls_read%20spark-admin%3Awholesale_subscribers_write%20spark-admin%3Acalling_cdr_read%20spark%3Arecordings_read%20cjp%3Atask_write%20spark-admin%3Ahybrid_clusters_read%20meeting%3Acontrols_read%20spark-admin%3Aworkspace_locations_write%20spark-admin%3Abroadworks_billing_reports_read%20spark-admin%3Atelephony_config_read%20spark-admin%3Abroadworks_enterprises_write%20meeting%3Aadmin_schedule_read%20spark-compliance%3Amemberships_write%20meeting%3Aschedules_read%20Identity%3AOrganization%20spark-admin%3Aroles_read%20spark-compliance%3Ameetings_read%20meeting%3Apreferences_read%20identity%3Apeople_read%20spark-admin%3Aworkspaces_read%20spark%3Atelephony_config_read%20spark-compliance%3Arooms_read%20spark-admin%3Abroadworks_subscribers_read%20identity%3Agroups_rw%20spark-admin%3Areports_write%20meeting%3Acontrols_write%20audit%3Aevents_read%20meeting%3Aadmin_recordings_write%20spark-compliance%3Arooms_write&state=set_state_here) \ No newline at end of file diff --git a/cloud/app.py b/cloud/app.py index 0580331..054999a 100644 --- a/cloud/app.py +++ b/cloud/app.py @@ -5,10 +5,14 @@ import uuid import datetime import sys import json -from flask import Flask, request, jsonify, current_app +import time +import urllib.parse +import requests +from flask import Flask, request, jsonify, current_app, redirect, url_for from pymongo import MongoClient from APIs import generate, EdamamAPI from APIs.mqtt import send_command +from APIs.webex import WebexManager from microwaveCookPlanner import MicrowaveCookPlanner import safety_checker @@ -29,9 +33,18 @@ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/") client = MongoClient(MONGO_URI) db = client["microwave_network_db"] +client_collection = db["client_data"] cooking_collection = db["cooking_parameters"] telemetry_collection = db["telemetry_data"] alert_collection = db["alert_data"] +webex_tokens_collection = db["webex_tokens"] # Collection for Webex OAuth tokens + +# Webex Credentials & Configuration from Environment Variables +WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID") +WEBEX_CLIENT_SECRET = os.getenv("WEBEX_CLIENT_SECRET", "YOUR_WEBEX_CLIENT_SECRET") +WEBEX_REDIRECT_URI = os.getenv("WEBEX_REDIRECT_URI", "https://smartwave.matthiasg.dev/oauth/callback") +WEBEX_TEAM_ID = os.getenv("WEBEX_TEAM_ID", "YOUR_WEBEX_TEAM_ID") +WEBEX_NINLUC_ID = os.getenv("WEBEX_NINLUC_ID", "YOUR_WEBEX_NINLUC_ID") # Ensure the camera image storage directory exists when the app starts CAMERA_IMAGE_DIR = "storage/dishPhotos" @@ -41,9 +54,49 @@ os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True) # Classes # --------------------------------------------------------- microwave_cook_planner = MicrowaveCookPlanner() +# Instantiate Webex Manager +webex_manager = WebexManager( + db_collection=webex_tokens_collection, + client_id=WEBEX_CLIENT_ID, + client_secret=WEBEX_CLIENT_SECRET, + redirect_uri=WEBEX_REDIRECT_URI, + team_id=WEBEX_TEAM_ID, + user_id=WEBEX_NINLUC_ID +) # --------------------------------------------------------- -# Routes +# OAuth Routes +# --------------------------------------------------------- + +@app.route("/oauth/login") +def oauth_login(): + """Redirects developer/admin to Webex for initial OAuth authorization.""" + scopes = "spark:rooms_write spark:rooms_read spark:memberships_write spark:memberships_read spark:messages_write" + params = { + "client_id": WEBEX_CLIENT_ID, + "response_type": "code", + "redirect_uri": WEBEX_REDIRECT_URI, + "scope": scopes + } + url = f"https://webexapis.com/v1/authorize?{urllib.parse.urlencode(params)}" + return redirect(url) + +@app.route("/oauth/callback") +def oauth_callback(): + """OAuth redirect endpoint that receives the authorization code.""" + code = request.args.get("code") + if not code: + return jsonify({"error": "Missing code parameter"}), 400 + + try: + webex_manager.exchange_code(code) + return jsonify({"status": "success", "message": "Webex tokens stored successfully in MongoDB!"}), 200 + except Exception as e: + current_app.logger.exception("Failed to exchange OAuth code") + return jsonify({"error": f"OAuth exchange failed: {str(e)}"}), 500 + +# --------------------------------------------------------- +# Application Routes # --------------------------------------------------------- @app.route("/") @@ -133,15 +186,34 @@ async def cooking_params(): except Exception as e: current_app.logger.exception("Exception in /cooking-params") + @app.route("/alert", methods=["POST"]) def alert(): - # Save the alert in alert_collection - data = request.get_json() + data = request.get_json() or {} alert_collection.insert_one(data) - # TODO : Créer une room webex et envoyer un sms - - return jsonify({"status": "success", "message": "Alert saved"}), 200 + # Extract client metadata from alert payload (or use fallback values) + client_name = data.get("client_name", "Unknown Client") + client_email = data.get("client_email") + + room_id = None + webex_status = "skipped" + + # Automatically create Webex room if email or client name is supplied + try: + room_id = webex_manager.create_support_room(client_name, client_email) + webex_status = "created" + except Exception as e: + current_app.logger.exception("Failed to create Webex support room") + webex_status = f"failed: {str(e)}" + + return jsonify({ + "status": "success", + "message": "Alert saved", + "webex_room_id": room_id, + "webex_status": webex_status + }), 200 + @app.route("/telemetry", methods=["POST"]) def telemetry():