From e5c4e8f54e1422ccb39eff6843afac68e3ed145b Mon Sep 17 00:00:00 2001 From: fortunesmith Date: Thu, 2 Apr 2026 16:01:57 -0400 Subject: [PATCH 1/2] Add wxc-call-intercept playbook (Webex Calling call intercept CLI) Made-with: Cursor --- playbooks/wxc-call-intercept/APPHUB.yaml | 119 +++++++++++ playbooks/wxc-call-intercept/README.md | 56 +++++ .../diagrams/architecture-diagram.md | 34 ++++ .../wxc-call-intercept/src/call_intercept.py | 192 ++++++++++++++++++ playbooks/wxc-call-intercept/src/env.template | 14 ++ .../wxc-call-intercept/src/requirements.txt | 20 ++ 6 files changed, 435 insertions(+) create mode 100644 playbooks/wxc-call-intercept/APPHUB.yaml create mode 100644 playbooks/wxc-call-intercept/README.md create mode 100644 playbooks/wxc-call-intercept/diagrams/architecture-diagram.md create mode 100644 playbooks/wxc-call-intercept/src/call_intercept.py create mode 100644 playbooks/wxc-call-intercept/src/env.template create mode 100644 playbooks/wxc-call-intercept/src/requirements.txt diff --git a/playbooks/wxc-call-intercept/APPHUB.yaml b/playbooks/wxc-call-intercept/APPHUB.yaml new file mode 100644 index 0000000..e52d97f --- /dev/null +++ b/playbooks/wxc-call-intercept/APPHUB.yaml @@ -0,0 +1,119 @@ +# APPHUB.yaml — Playbook metadata for Webex App Hub +# Copy this file into your Playbook folder under playbooks// +# Fill in all required fields. See CONTRIBUTING.md for field rules. + +# ----------------------------------------------------------------------------- +# friendly_id — Unique identifier. Must end with -playbook (e.g. epic-ehr-playbook) +# ----------------------------------------------------------------------------- +friendly_id: "wxc-call-intercept-playbook" + +# ----------------------------------------------------------------------------- +# title — Display name for the Playbook (matches ContentStack field) +# ----------------------------------------------------------------------------- +title: "Webex Calling Call Intercept Management" + +# ----------------------------------------------------------------------------- +# tag_line — Short tagline for App Hub detail page (required, max 128 chars) +# ----------------------------------------------------------------------------- +tag_line: "Python sample to read or toggle Webex Calling user call intercept settings with wxc_sdk" + +# ----------------------------------------------------------------------------- +# description — App Hub supports Markdown. Use a block scalar (description: |) with +# blank lines between sections: opening paragraph (bold key terms), **Why use this +# playbook** (3–5 outcome bullets), **What it does** (concrete behaviors/endpoints). +# Do not put upstream repo URLs or install-only instructions here—use README and +# src/README.md for reference playbooks. See docs/commands/import_playbook.md. +# ----------------------------------------------------------------------------- +description: | + A **Python** CLI that uses **wxc_sdk** and documented **Webex Calling** APIs to **read** + and **toggle** **call intercept** (on/off) for a user identified by **email**. + + **Why use this playbook** + + - **Ship faster:** Start from a working **People** lookup plus **person_settings.call_intercept** + flow instead of reverse-engineering payloads from scratch. + - **Flexible auth:** Supports a passed-in token, **WEBEX_ACCESS_TOKEN**, or **OAuth** via + integration credentials loaded from the environment. + - **Secrets-safe pattern:** Integration IDs, secrets, and tokens are intended for + **environment variables** (see **env.template**), not hardcoded values. + - **Easy to verify:** Single-user CLI with optional **debug** logging for REST traces during + development. + + **What it does** + + - Resolves **person_id** by listing **People** with the target **email**. + - Calls **person_settings.call_intercept.read** to print whether intercept is **on** or **off**. + - Optionally calls **person_settings.call_intercept.configure** when you pass **on** or **off**, + then re-reads to confirm the new state. + - Can obtain tokens through a local **OAuth** redirect to **localhost:6001** and cache them in + a YAML file next to the script. + +# ----------------------------------------------------------------------------- +# product_types — Where this Playbook appears. Pick one or more. +# Valid: teams | meetings | calling | rooms | contact_center +# ----------------------------------------------------------------------------- +product_types: + - "calling" + +# ----------------------------------------------------------------------------- +# categories — App Hub category slugs. Pick one or more. +# Verticals: healthcare | financial-services | retail-ecommerce +# App categories (use kebab-case slugs): +# ai-agent-testing-observability | agent-supervisor-tools | analytics | +# calendar-scheduling | collaboration-management | customer-relations | +# customer-support | developer-tools | doc-management | education | +# finance | government | healthcare | human-resources | internet-of-things | +# marketing-sales | orchestration | platform | productivity | +# project-management | recording-transcriptions | security-compliance | +# self-service-bots | social-and-fun | strategy-team-planning | +# workflow-automation | workforce-optimization | other +# ----------------------------------------------------------------------------- +categories: + - "developer-tools" + - "productivity" + +# ----------------------------------------------------------------------------- +# company_name — Your company or team name +# ----------------------------------------------------------------------------- +company_name: "Webex for Developers" + +# ----------------------------------------------------------------------------- +# company_url — Your company or project URL +# ----------------------------------------------------------------------------- +company_url: "https://developer.webex.com" + +# ----------------------------------------------------------------------------- +# support_url — Issues or support link (e.g. GitHub issues) +# ----------------------------------------------------------------------------- +support_url: "https://github.com/webex/webexplaybooks/issues" + +# ----------------------------------------------------------------------------- +# product_url — Link to this Playbook in the repo (required) +# ----------------------------------------------------------------------------- +product_url: "https://github.com/webex/webexplaybooks/tree/main/playbooks/wxc-call-intercept" + +# ----------------------------------------------------------------------------- +# logo — (Optional) URL to your logo image. If not provided, defaults to the +# standard Webex Playbook logo. +# ----------------------------------------------------------------------------- +logo: "https://images.contentstack.io/v3/assets/bltd14fd2a03236233f/blta2de9daa773c6604/60f71f81e2de935fc7e35dbe/download" + +# ----------------------------------------------------------------------------- +# estimated_implementation_time — e.g. "2-4 hours", "1 day" +# ----------------------------------------------------------------------------- +estimated_implementation_time: "1-2 hours" + +# ----------------------------------------------------------------------------- +# third_party_tool — (Optional) The tool being integrated (e.g. Salesforce, Epic) +# Omit for generic playbooks (e.g. "any CMS") +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# privacy_url — Privacy policy URL (required; use Cisco default for Webex-authored) +# ----------------------------------------------------------------------------- +privacy_url: "https://www.cisco.com/c/en/us/about/legal/privacy-full.html" + +# ----------------------------------------------------------------------------- +# submission_date — (Optional) ISO date (e.g. 2025-03-01) +# ----------------------------------------------------------------------------- +submission_date: "2026-04-02" diff --git a/playbooks/wxc-call-intercept/README.md b/playbooks/wxc-call-intercept/README.md new file mode 100644 index 0000000..6801b50 --- /dev/null +++ b/playbooks/wxc-call-intercept/README.md @@ -0,0 +1,56 @@ +# Webex Calling Call Intercept Management + +This Playbook is adapted from the [wxc_call_intercept](https://github.com/jeokrohn/wxc_call_intercept) sample on GitHub. + +## Use Case Overview + +Administrators and developers need a quick, scriptable way to **read** whether **call intercept** is enabled for a **Webex Calling** user and to **turn it on or off** without opening Control Hub for every change. This Playbook provides a minimal **Python** CLI that uses the **wxc_sdk** against documented **Webex** person-settings APIs. It is aimed at **IT admins**, **integration developers**, and **partners** automating user telephony settings. **Estimated implementation time:** 1–2 hours (integration app or token setup, Python environment, first successful run). + +## Architecture + +The sample runs **locally** as a command-line script. It authenticates with a **Webex access token** (CLI argument, `WEBEX_ACCESS_TOKEN`, or **OAuth** via integration credentials and a localhost redirect), resolves the target user by **email** with the **People** API, then calls **person settings → call intercept** to **read** the current state and optionally **configure** intercept on or off. Authentication and REST traffic go to **Webex** cloud endpoints; no separate third-party service is involved. For a sequence view of token acquisition, user lookup, and intercept read/update, see [diagrams/architecture-diagram.md](diagrams/architecture-diagram.md). + +## Prerequisites + +- **Webex organization** with **Webex Calling** licensed for the target users. +- **API access** sufficient to: + - List people by email and read `personId` (People API). + - Read and update **call intercept** person settings for those users (scopes must match your chosen auth path—typically an **admin** or **integration** with the appropriate **spark** scopes; align with your integration or token documentation). +- One of the following **authentication** approaches: + - A valid **access token** passed as `--token` or in `WEBEX_ACCESS_TOKEN`, **or** + - A **Webex Integration** with **Client ID**, **Client Secret**, and **scopes** configured; the sample uses OAuth with redirect `http://localhost:6001/redirect` and persists tokens to a local YAML file next to the script. +- **Developer environment:** **Python 3** (3.8+ recommended) and **pip**. +- **Network:** Outbound **HTTPS** to Webex API hosts; for OAuth, local browser access to **localhost:6001** during token acquisition. +- **Compliance:** Only run against users and orgs you are authorized to administer. + +## Code Scaffold + +Under **`src/`**: + +| File | Purpose | +|------|---------| +| `call_intercept.py` | CLI: `user_email` plus optional `on`/`off`; reads intercept state; updates if `on` or `off` is supplied. | +| `env.template` | Copy to `.env` (or export variables) for **OAuth** integration parameters. | +| `requirements.txt` | Pinned Python dependencies (including **wxc-sdk**) for a reproducible install. | + +The script demonstrates **WebexSimpleApi**, **People** listing, and **person_settings.call_intercept** **read**/**configure**. It does **not** provide production logging, retries, bulk operations, secure token storage, or Control Hub UI parity. **Secrets** belong in **environment variables** or your secret manager—not in source control. + +## Deployment Guide + +1. **Clone this repository** (or copy `playbooks/wxc-call-intercept/src/`) to your machine. +2. **Create a Python virtual environment** (recommended): `python3 -m venv .venv && source .venv/bin/activate` (on Windows, `.venv\Scripts\activate`). +3. **Install dependencies:** from `playbooks/wxc-call-intercept/src/`, run `pip install -r requirements.txt`. +4. **Choose authentication:** + - **Option A — Existing token:** Obtain an access token with scopes that allow people lookup and call intercept person settings. Export `WEBEX_ACCESS_TOKEN` or pass `--token `. + - **Option B — Integration OAuth:** In [Webex for Developers](https://developer.webex.com), create an **Integration** with redirect URI **`http://localhost:6001/redirect`**. Copy **`src/env.template`** to **`.env`** in your working directory (or set exports) and fill `TOKEN_INTEGRATION_CLIENT_ID`, `TOKEN_INTEGRATION_CLIENT_SECRET`, and `TOKEN_INTEGRATION_CLIENT_SCOPES`. +5. **Run from the directory containing your `.env`** (if using OAuth): `python call_intercept.py ` to print `on` or `off`. +6. **Update intercept:** `python call_intercept.py on` or `... off` to set intercept and print confirmation. +7. **Debug REST traffic (optional):** add `-d` / `--debug` for verbose logging. + +## Known Limitations + +- **Rate limits** and throttling apply per **Webex** API policy; the sample does not implement backoff or batching. +- **Access tokens expire**; refresh or re-run OAuth as appropriate. Token cache is a **local YAML** file—**not** suitable for shared or production systems without hardening. +- The upstream sample repository did not include a **`LICENSE`** file in the checked-out tree; confirm license terms on the [source repository](https://github.com/jeokrohn/wxc_call_intercept) before redistribution. This Playbook’s use is also subject to the playbook repository [LICENSE](../../LICENSE). +- **SDK and API versions** may change; validate behavior after upgrading **wxc-sdk**. +- This Playbook is provided as a starting point. Webex does not guarantee the functional accuracy of the source code. Test thoroughly before use in a production environment. diff --git a/playbooks/wxc-call-intercept/diagrams/architecture-diagram.md b/playbooks/wxc-call-intercept/diagrams/architecture-diagram.md new file mode 100644 index 0000000..ff4a881 --- /dev/null +++ b/playbooks/wxc-call-intercept/diagrams/architecture-diagram.md @@ -0,0 +1,34 @@ +# Architecture — Webex Calling call intercept CLI + +This diagram shows how the sample CLI authenticates, resolves a user, and reads or updates **call intercept** via **Webex** APIs. + +```mermaid +sequenceDiagram + participant Admin as AdminOrDeveloper + participant CLI as call_intercept.py + participant Token as TokenSource + participant Webex as WebexAPIS + + Admin->>CLI: Run with user email optional on/off + alt CLI token or WEBEX_ACCESS_TOKEN + CLI->>Token: Use provided access token string + else OAuth integration env vars + CLI->>Token: build_integration TOKEN_INTEGRATION_* + Token->>Webex: OAuth authorize redirect localhost:6001 + Admin->>Token: Browser completes login consent + Token-->>CLI: Access token cached in local YAML + end + CLI->>Webex: People list filter by email + Webex-->>CLI: person_id + CLI->>Webex: person_settings.call_intercept.read + Webex-->>CLI: intercept enabled state + alt Optional on or off argument + CLI->>Webex: person_settings.call_intercept.configure + Webex-->>CLI: Success + CLI->>Webex: person_settings.call_intercept.read + Webex-->>CLI: Updated state + end + CLI-->>Admin: Print on or off set to on/off +``` + +Note: Sequence labels use simplified endpoint names; the **wxc_sdk** maps these to documented REST resources. diff --git a/playbooks/wxc-call-intercept/src/call_intercept.py b/playbooks/wxc-call-intercept/src/call_intercept.py new file mode 100644 index 0000000..1829eb6 --- /dev/null +++ b/playbooks/wxc-call-intercept/src/call_intercept.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +# +# Playbook scaffold — Webex Calling call intercept CLI (adapted from jeokrohn/wxc_call_intercept). +# +# What this does: +# Reads call intercept (enabled/disabled) for a Webex Calling user by email; optionally sets on/off. +# +# What this does NOT do: +# Production hardening, bulk updates, secure team-wide token storage, or Control Hub UI. +# +# Configuration: +# See env.template. Use WEBEX_ACCESS_TOKEN or --token, or OAuth via TOKEN_INTEGRATION_* variables. +# +# Dependencies: +# pip install -r requirements.txt +# +"""Script to read/update call intercept settings of a calling user. + +The script uses the access token passed via the CLI, reads one from the WEBEX_ACCESS_TOKEN environment variable or +obtains tokens via an OAuth flow. + + usage: call_intercept.py [-h] [--token TOKEN] [-d] user_email [{on,off}] + + positional arguments: + user_email email address of user + {on,off} operation to apply + + options: + -h, --help show this help message and exit + --token TOKEN admin access token to use + -d, --debug show detailed REST trace +""" +import argparse +import logging +import os +import re +import sys +from json import loads +from typing import Optional + +from dotenv import load_dotenv +from wxc_sdk import WebexSimpleApi +from wxc_sdk.integration import Integration +from wxc_sdk.person_settings.call_intercept import InterceptSetting +from wxc_sdk.rest import RestError +from wxc_sdk.scopes import parse_scopes +from wxc_sdk.tokens import Tokens +from yaml import safe_dump, safe_load + +log = logging.getLogger(__name__) + + +def yml_path() -> str: + """ + determine path of YML file to persist tokens + + :return: path to YML file + :rtype: str + """ + return os.path.join(os.getcwd(), f'{os.path.splitext(os.path.basename(__file__))[0]}.yml') + + +def build_integration() -> Integration: + """ + read integration parameters from environment variables and create an integration + + :return: :class:`wxc_sdk.integration.Integration` instance + """ + client_id = os.getenv('TOKEN_INTEGRATION_CLIENT_ID') + client_secret = os.getenv('TOKEN_INTEGRATION_CLIENT_SECRET') + scopes = os.getenv('TOKEN_INTEGRATION_CLIENT_SCOPES') + if scopes: + scopes = parse_scopes(scopes) + if not all((client_id, client_secret, scopes)): + raise ValueError('failed to get integration parameters from environment') + redirect_url = 'http://localhost:6001/redirect' + return Integration(client_id=client_id, client_secret=client_secret, scopes=scopes, + redirect_url=redirect_url) + + +def get_tokens() -> Optional[Tokens]: + """ + + Tokens are read from a YML file. If needed an OAuth flow is initiated. + + :return: tokens + :rtype: :class:`wxc_sdk.tokens.Tokens` + """ + + def write_tokens(tokens_to_cache: Tokens): + with open(yml_path(), mode='w') as f: + safe_dump(loads(tokens_to_cache.json()), f) + return + + def read_tokens() -> Optional[Tokens]: + try: + with open(yml_path(), mode='r') as f: + data = safe_load(f) + tokens_read = Tokens.parse_obj(data) + except Exception as e: + log.info(f'failed to read tokens from file: {e}') + tokens_read = None + return tokens_read + + integration = build_integration() + tokens = integration.get_cached_tokens(read_from_cache=read_tokens, + write_to_cache=write_tokens) + return tokens + + +RE_EMAIL = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") + + +def email_type(value): + if not RE_EMAIL.match(value): + raise argparse.ArgumentTypeError(f"'{value}' is not a valid email") + return value + + +def main(): + """ + where the magic happens + """ + parser = argparse.ArgumentParser() + parser.add_argument('user_email', type=email_type, help='email address of user') + parser.add_argument('on_off', choices=['on', 'off'], nargs='?', help='operation to apply') + parser.add_argument('--token', type=str, required=False, help='admin access token to use') + parser.add_argument('-d', '--debug', action='store_true', help='show detailed REST trace') + args = parser.parse_args() + + # read .env file with the settings for the integration to be used to obtain tokens + load_dotenv() + + if args.token: + tokens = args.token + elif (tokens := os.getenv('WEBEX_ACCESS_TOKEN')) is None: + tokens = get_tokens() + + if not tokens: + print('Failed to get tokens', file=sys.stderr) + exit(1) + + # set level to DEBUG to see debug of REST requests if requested on the command line or executed in a debugger + if args.debug or ((gt := getattr(sys, 'gettrace', None)) and gt()): + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + with WebexSimpleApi(tokens=tokens) as api: + # get user + email = args.user_email.lower() + user = next((user + for user in api.people.list(email=email) + if user.emails[0] == email), None) + if user is None: + print(f'User "{email}" not found', file=sys.stderr) + exit(1) + + # display call intercept status + try: + intercept = api.person_settings.call_intercept.read(person_id=user.person_id) + except RestError as e: + print(f'Failed to read call intercept settings: {e.response.status_code}, {e.description}') + exit(1) + + print('on' if intercept.enabled else 'off') + if args.on_off: + # action: turn on/off + intercept = InterceptSetting.default() + intercept.enabled = args.on_off == 'on' + try: + api.person_settings.call_intercept.configure(person_id=user.person_id, + intercept=intercept) + except RestError as e: + print(f'Failed to update call intercept settings: {e.response.status_code}, {e.description}') + exit(1) + + # read call intercept again + try: + intercept = api.person_settings.call_intercept.read(person_id=user.person_id) + except RestError as e: + print(f'Failed to read call intercept settings: {e.response.status_code}, {e.description}') + exit(1) + + # display state after update + print(f"set to {'on' if intercept.enabled else 'off'}") + + exit(0) + + +if __name__ == '__main__': + main() diff --git a/playbooks/wxc-call-intercept/src/env.template b/playbooks/wxc-call-intercept/src/env.template new file mode 100644 index 0000000..629f369 --- /dev/null +++ b/playbooks/wxc-call-intercept/src/env.template @@ -0,0 +1,14 @@ +# Copy to .env in your working directory (same folder you run the script from), or export these variables. +# Used only when you are NOT passing --token or WEBEX_ACCESS_TOKEN — the script will run OAuth and cache tokens in call_intercept.yml. +# +# TOKEN_INTEGRATION_CLIENT_ID — Webex Integration Client ID from developer.webex.com +TOKEN_INTEGRATION_CLIENT_ID= +# +# TOKEN_INTEGRATION_CLIENT_SECRET — Webex Integration Client Secret +TOKEN_INTEGRATION_CLIENT_SECRET= +# +# TOKEN_INTEGRATION_CLIENT_SCOPES — Scopes your integration is granted (space-separated list, or quoted string, per wxc_sdk.parse_scopes) +TOKEN_INTEGRATION_CLIENT_SCOPES="" +# +# Optional: static access token (overridden by CLI --token if passed) +# WEBEX_ACCESS_TOKEN= diff --git a/playbooks/wxc-call-intercept/src/requirements.txt b/playbooks/wxc-call-intercept/src/requirements.txt new file mode 100644 index 0000000..c19b324 --- /dev/null +++ b/playbooks/wxc-call-intercept/src/requirements.txt @@ -0,0 +1,20 @@ +aiohttp==3.8.3 +aiosignal==1.2.0 +async-timeout==4.0.2 +attrs==22.1.0 +backoff==2.2.1 +certifi==2022.9.24 +charset-normalizer==2.1.1 +frozenlist==1.3.1 +idna==3.4 +multidict==6.0.2 +pydantic==1.10.2 +python-dotenv==0.21.0 +pytz==2022.4 +PyYAML==6.0 +requests==2.28.1 +requests-toolbelt==0.9.1 +typing_extensions==4.4.0 +urllib3==1.26.12 +wxc-sdk==1.6.0 +yarl==1.8.1 From 7dd9c0ff322b52d6c0087f7d72451cd8d79e5e30 Mon Sep 17 00:00:00 2001 From: fortunesmith Date: Thu, 2 Apr 2026 16:06:45 -0400 Subject: [PATCH 2/2] wxc-call-intercept: pin deps for Python 3.13 and urllib3<2 (requests-toolbelt) Made-with: Cursor --- .../wxc-call-intercept/src/requirements.txt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/playbooks/wxc-call-intercept/src/requirements.txt b/playbooks/wxc-call-intercept/src/requirements.txt index c19b324..7d1ee20 100644 --- a/playbooks/wxc-call-intercept/src/requirements.txt +++ b/playbooks/wxc-call-intercept/src/requirements.txt @@ -1,20 +1,23 @@ -aiohttp==3.8.3 -aiosignal==1.2.0 -async-timeout==4.0.2 -attrs==22.1.0 +# Pinned for reproducibility. urllib3<2 is required for requests-toolbelt 0.9.x (wxc-sdk 1.6.0). +# Tested with Python 3.13 on macOS ARM64. +aiohappyeyeballs==2.6.1 +aiohttp==3.13.5 +aiosignal==1.4.0 +attrs==26.1.0 backoff==2.2.1 -certifi==2022.9.24 -charset-normalizer==2.1.1 -frozenlist==1.3.1 -idna==3.4 -multidict==6.0.2 -pydantic==1.10.2 -python-dotenv==0.21.0 -pytz==2022.4 -PyYAML==6.0 -requests==2.28.1 +certifi==2026.2.25 +charset-normalizer==3.4.7 +frozenlist==1.8.0 +idna==3.11 +multidict==6.7.1 +propcache==0.4.1 +pydantic==1.10.26 +python-dotenv==1.2.2 +pytz==2026.1.post1 +PyYAML==6.0.2 +requests==2.33.1 requests-toolbelt==0.9.1 -typing_extensions==4.4.0 -urllib3==1.26.12 +typing_extensions==4.15.0 +urllib3==1.26.20 wxc-sdk==1.6.0 -yarl==1.8.1 +yarl==1.23.0