Webex first draft
Build, push image, and notify Watchtower / build-image (push) Successful in 1m0s
Build, push image, and notify Watchtower / notify (push) Successful in 17s

This commit is contained in:
2026-08-17 14:21:02 +02:00
parent 8b6ab086fd
commit b442d39c18
3 changed files with 195 additions and 8 deletions
+111
View File
@@ -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
+4
View File
@@ -2,3 +2,7 @@
in `/cloud` folder :
`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)
+78 -6
View File
@@ -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
# 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
return jsonify({"status": "success", "message": "Alert saved"}), 200
@app.route("/telemetry", methods=["POST"])
def telemetry():