diff --git a/server/plaud_mcp.py b/server/plaud_mcp.py index 676bce9..b98e1cf 100644 --- a/server/plaud_mcp.py +++ b/server/plaud_mcp.py @@ -1,338 +1 @@ -#!/usr/bin/env python3 -""" -Plaud MCP Server for MPM -Connects to the Plaud API and exposes tools for listing, searching, -and reading transcripts, summaries, and notes from Plaud recordings. - -Credentials: reads token from ~/.plaud/config.json (written by plaud-connector -import-token command). Falls back to PLAUD_TOKEN env var. -""" - -import json -import os -import urllib.request -import urllib.error -import urllib.parse -from datetime import datetime, timezone -from pathlib import Path -from typing import Optional - -from mcp.server.fastmcp import FastMCP - -# ── Configuration ───────────────────────────────────────────────────────────── -# US region: api.plaud.ai | EU region: api.plaud.eu -PLAUD_REGION = os.environ.get("PLAUD_REGION", "us") -BASE_URL = "https://api.plaud.eu" if PLAUD_REGION == "eu" else "https://api.plaud.ai" -CONFIG_PATH = Path.home() / ".plaud" / "config.json" - - -# ── Token resolution ────────────────────────────────────────────────────────── - -def _load_token() -> Optional[str]: - """ - Resolve the Plaud bearer token. Priority: - 1. PLAUD_TOKEN env var - 2. ~/.plaud/config.json → token.token (plaud-connector format) - 3. ~/.plaud/config.json → token (bare string fallback) - """ - env_token = os.environ.get("PLAUD_TOKEN", "").strip() - if env_token: - return env_token - - if CONFIG_PATH.exists(): - try: - raw = CONFIG_PATH.read_text() - data = json.loads(raw) - # plaud-connector / plaud-toolkit format: { "token": { "token": "...", "expiresAt": ... } } - token_block = data.get("token") - if isinstance(token_block, dict): - t = token_block.get("token", "").strip() - if t: - return t - # bare string fallback - if isinstance(token_block, str) and token_block.strip(): - return token_block.strip() - except Exception: - pass - - return None - - -def _get_token() -> str: - token = _load_token() - if not token: - raise RuntimeError( - "No Plaud token found. Run:\n" - " npx tsx ~/Developer/plaud-connector/packages/cli/bin/plaud.ts import-token app \"\"\n" - "or set the PLAUD_TOKEN environment variable." - ) - return token - - -# ── HTTP helpers ────────────────────────────────────────────────────────────── - -def _api_get(path: str, params: Optional[dict] = None) -> dict: - """GET request to the Plaud API. Returns parsed JSON.""" - token = _get_token() - url = f"{BASE_URL}{path}" - if params: - query = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items() if v is not None) - if query: - url = f"{url}?{query}" - - req = urllib.request.Request(url) - req.add_header("Authorization", f"Bearer {token}") - req.add_header("Content-Type", "application/json") - req.add_header("Accept", "application/json") - req.add_header("User-Agent", "plaud-mpm-cowork/0.1.0") - - try: - with urllib.request.urlopen(req, timeout=30) as resp: - return json.loads(resp.read().decode("utf-8")) - except urllib.error.HTTPError as e: - body = e.read().decode("utf-8", errors="replace") - raise RuntimeError(f"Plaud API error {e.code}: {body[:300]}") - except urllib.error.URLError as e: - raise RuntimeError(f"Network error reaching Plaud API: {e.reason}") - - -# ── Data helpers ────────────────────────────────────────────────────────────── - -def _fmt_duration(ms: int) -> str: - mins = ms // 60000 - if mins < 60: - return f"{mins}m" - return f"{mins // 60}h {mins % 60}m" - - -def _fmt_date(ts_ms: int) -> str: - try: - dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc) - return dt.strftime("%Y-%m-%d %H:%M UTC") - except Exception: - return str(ts_ms) - - -def _summarise_recording(r: dict) -> dict: - """Return a compact dict suitable for listing.""" - return { - "id": r.get("id", ""), - "title": r.get("filename") or r.get("title") or "(untitled)", - "date": _fmt_date(r.get("start_time", 0)), - "duration": _fmt_duration(r.get("duration", 0)), - "has_transcript": bool(r.get("is_trans")), - "has_summary": bool(r.get("auto_sum_note") or r.get("is_sum")), - } - - -# ── MCP server ──────────────────────────────────────────────────────────────── -mcp = FastMCP("plaud-mpm") - - -@mcp.tool() -def plaud_list_recordings( - limit: int = 50, - only_with_transcript: bool = False, -) -> str: - """ - List Plaud recordings newest-first. - - Args: - limit: Max number to return (default 50, max 200). - only_with_transcript: If true, only return recordings that have a transcript. - """ - resp = _api_get("/file/simple/web") - recordings = resp.get("data", resp) if isinstance(resp, dict) else resp - if not isinstance(recordings, list): - recordings = [] - - if only_with_transcript: - recordings = [r for r in recordings if r.get("is_trans")] - - recordings = recordings[:min(limit, 200)] - result = [_summarise_recording(r) for r in recordings] - return json.dumps(result, indent=2) - - -@mcp.tool() -def plaud_search_recordings( - title_contains: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - only_with_transcript: bool = False, - max_results: int = 20, -) -> str: - """ - Search recordings by title and/or date range. - - Args: - title_contains: Substring to match in the recording title (case-insensitive). - start_date: ISO date string, e.g. '2026-04-01'. Only recordings on or after this date. - end_date: ISO date string, e.g. '2026-05-01'. Only recordings on or before this date. - only_with_transcript: If true, only return recordings that have a transcript. - max_results: Max number of results to return (default 20). - """ - resp = _api_get("/file/simple/web") - recordings = resp.get("data", resp) if isinstance(resp, dict) else resp - if not isinstance(recordings, list): - recordings = [] - - def matches(r: dict) -> bool: - if only_with_transcript and not r.get("is_trans"): - return False - title = (r.get("filename") or r.get("title") or "").lower() - if title_contains and title_contains.lower() not in title: - return False - if start_date or end_date: - ts_ms = r.get("start_time", 0) - rec_date = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).date() - if start_date: - if rec_date < datetime.fromisoformat(start_date).date(): - return False - if end_date: - if rec_date > datetime.fromisoformat(end_date).date(): - return False - return True - - matched = [r for r in recordings if matches(r)][:max_results] - return json.dumps([_summarise_recording(r) for r in matched], indent=2) - - -@mcp.tool() -def plaud_get_transcript(recording_id: str) -> str: - """ - Get the full transcript for a Plaud recording. - - Args: - recording_id: The recording ID (from plaud_list_recordings). - """ - resp = _api_get(f"/file/detail/{recording_id}") - data = resp.get("data", resp) if isinstance(resp, dict) else resp - - title = data.get("filename") or data.get("title") or "(untitled)" - transcript = data.get("transcript") or "" - - # Some recordings store transcript in source_list segments - if not transcript: - source_list = data.get("source_list") or [] - if source_list: - transcript = "\n".join( - f"[{s.get('speaker', '')}] {s.get('content', '')}" if s.get("speaker") - else s.get("content", "") - for s in source_list - ) - - return json.dumps({ - "id": recording_id, - "title": title, - "date": _fmt_date(data.get("start_time", 0)), - "duration": _fmt_duration(data.get("duration", 0)), - "transcript": transcript or "No transcript available for this recording.", - }, indent=2) - - -@mcp.tool() -def plaud_get_summary(recording_id: str) -> str: - """ - Get the AI-generated summary for a Plaud recording. - - Args: - recording_id: The recording ID (from plaud_list_recordings). - """ - resp = _api_get(f"/file/detail/{recording_id}") - data = resp.get("data", resp) if isinstance(resp, dict) else resp - - title = data.get("filename") or data.get("title") or "(untitled)" - summary = data.get("auto_sum_note") or "" - - return json.dumps({ - "id": recording_id, - "title": title, - "date": _fmt_date(data.get("start_time", 0)), - "summary": summary or "No AI summary available for this recording.", - }, indent=2) - - -@mcp.tool() -def plaud_get_notes(recording_id: str) -> str: - """ - Get AI-generated notes and action items for a Plaud recording. - Returns all note entries from the content_list (action items, key points, etc.). - - Args: - recording_id: The recording ID (from plaud_list_recordings). - """ - resp = _api_get(f"/file/detail/{recording_id}") - data = resp.get("data", resp) if isinstance(resp, dict) else resp - - title = data.get("filename") or data.get("title") or "(untitled)" - content_list = data.get("content_list") or [] - - notes = [] - for item in content_list: - note_entry = { - "type": item.get("data_type") or item.get("template_type") or "note", - "title": item.get("template_name") or item.get("tab_name") or "", - "content": item.get("note") or item.get("content") or "", - } - if note_entry["content"]: - notes.append(note_entry) - - return json.dumps({ - "id": recording_id, - "title": title, - "date": _fmt_date(data.get("start_time", 0)), - "notes": notes if notes else [{"type": "none", "content": "No notes available for this recording."}], - }, indent=2) - - -@mcp.tool() -def plaud_get_recording_detail(recording_id: str) -> str: - """ - Get full metadata for a Plaud recording including title, date, duration, - transcript availability, summary availability, and note count. - - Args: - recording_id: The recording ID (from plaud_list_recordings). - """ - resp = _api_get(f"/file/detail/{recording_id}") - data = resp.get("data", resp) if isinstance(resp, dict) else resp - - return json.dumps({ - "id": recording_id, - "title": data.get("filename") or data.get("title") or "(untitled)", - "date": _fmt_date(data.get("start_time", 0)), - "duration": _fmt_duration(data.get("duration", 0)), - "has_transcript": bool(data.get("is_trans")), - "has_summary": bool(data.get("auto_sum_note") or data.get("is_sum")), - "note_count": len(data.get("content_list") or []), - "language": data.get("language") or "unknown", - "device": data.get("device_name") or data.get("device") or "unknown", - "tags": data.get("filetag") or [], - }, indent=2) - - -@mcp.tool() -def plaud_user_info() -> str: - """Get the current Plaud account information.""" - try: - resp = _api_get("/user/info") - data = resp.get("data", resp) if isinstance(resp, dict) else resp - return json.dumps({ - "email": data.get("email") or data.get("username") or "unknown", - "name": data.get("name") or data.get("display_name") or "", - "region": PLAUD_REGION, - "api_base": BASE_URL, - "token_loaded": bool(_load_token()), - }, indent=2) - except Exception as e: - return json.dumps({ - "region": PLAUD_REGION, - "api_base": BASE_URL, - "token_loaded": bool(_load_token()), - "note": str(e), - }, indent=2) - - -if __name__ == "__main__": - mcp.run() +IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKUGxhdWQgTUNQIFNlcnZlciBmb3IgTVBNCkNvbm5lY3RzIHRvIHRoZSBQbGF1ZCBBUEkgYW5kIGV4cG9zZXMgdG9vbHMgZm9yIGxpc3RpbmcsIHNlYXJjaGluZywKYW5kIHJlYWRpbmcgdHJhbnNjcmlwdHMsIHN1bW1hcmllcywgYW5kIG5vdGVzIGZyb20gUGxhdWQgcmVjb3JkaW5ncy4KCkNyZWRlbnRpYWxzOiByZWFkcyB0b2tlbiBmcm9tIH4vLnBsYXVkL2NvbmZpZy5qc29uLgoiIiIKCmltcG9ydCBnemlwCmltcG9ydCBqc29uCmltcG9ydCBvcwppbXBvcnQgc3VicHJvY2VzcwppbXBvcnQgdXJsbGliLnBhcnNlCmZyb20gZGF0ZXRpbWUgaW1wb3J0IGRhdGV0aW1lLCB0aW1lem9uZQpmcm9tIHBhdGhsaWIgaW1wb3J0IFBhdGgKZnJvbSB0eXBpbmcgaW1wb3J0IE9wdGlvbmFsCgpmcm9tIG1jcC5zZXJ2ZXIuZmFzdG1jcCBpbXBvcnQgRmFzdE1DUAoKIyDilIDilIAgQ29uZmlndXJhdGlvbiDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKUExBVURfUkVHSU9OID0gb3MuZW52aXJvbi5nZXQoIlBMQVVEX1JFR0lPTiIsICJ1cyIpCkJBU0VfVVJMID0gImh0dHBzOi8vYXBpLnBsYXVkLmV1IiBpZiBQTEFVRF9SRUdJT04gPT0gImV1IiBlbHNlICJodHRwczovL2FwaS5wbGF1ZC5haSIKQ09ORklHX1BBVEggPSBQYXRoLmhvbWUoKSAvICIucGxhdWQiIC8gImNvbmZpZy5qc29uIgoKCiMg4pSA4pSAIFRva2VuIHJlc29sdXRpb24g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpkZWYgX2xvYWRfdG9rZW4oKSAtPiBPcHRpb25hbFtzdHJdOgogICAgZW52X3Rva2VuID0gb3MuZW52aXJvbi5nZXQoIlBMQVVEX1RPS0VOIiwgIiIpLnN0cmlwKCkKICAgIGlmIGVudl90b2tlbjoKICAgICAgICByZXR1cm4gZW52X3Rva2VuCgogICAgaWYgQ09ORklHX1BBVEguZXhpc3RzKCk6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYXRhID0ganNvbi5sb2FkcyhDT05GSUdfUEFUSC5yZWFkX3RleHQoKSkKICAgICAgICAgICAgdG9rZW5fYmxvY2sgPSBkYXRhLmdldCgidG9rZW4iKQogICAgICAgICAgICBpZiBpc2luc3RhbmNlKHRva2VuX2Jsb2NrLCBkaWN0KToKICAgICAgICAgICAgICAgIHQgPSAodG9rZW5fYmxvY2suZ2V0KCJ0b2tlbiIpIG9yIHRva2VuX2Jsb2NrLmdldCgiYWNjZXNzVG9rZW4iKSBvciAiIikuc3RyaXAoKQogICAgICAgICAgICAgICAgaWYgdDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gdAogICAgICAgICAgICBpZiBpc2luc3RhbmNlKHRva2VuX2Jsb2NrLCBzdHIpIGFuZCB0b2tlbl9ibG9jay5zdHJpcCgpOgogICAgICAgICAgICAgICAgcmV0dXJuIHRva2VuX2Jsb2NrLnN0cmlwKCkKICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICBwYXNzCgogICAgcmV0dXJuIE5vbmUKCgpkZWYgX2dldF90b2tlbigpIC0+IHN0cjoKICAgIHRva2VuID0gX2xvYWRfdG9rZW4oKQogICAgaWYgbm90IHRva2VuOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigKICAgICAgICAgICAgIk5vIFBsYXVkIHRva2VuIGZvdW5kLiBSdW4gdGhlIHBsYXVkLWNvbm5lY3RvciBpbXBvcnQtdG9rZW4gY29tbWFuZCAiCiAgICAgICAgICAgICJvciBzZXQgdGhlIFBMQVVEX1RPS0VOIGVudmlyb25tZW50IHZhcmlhYmxlLiIKICAgICAgICApCiAgICByZXR1cm4gdG9rZW4KCgojIOKUgOKUgCBIVFRQIGhlbHBlcnMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpkZWYgX2FwaV9nZXQocGF0aDogc3RyLCBwYXJhbXM6IE9wdGlvbmFsW2RpY3RdID0gTm9uZSkgLT4gZGljdDoKICAgICIiIkdFVCByZXF1ZXN0IHRvIHRoZSBQbGF1ZCBBUEkgdXNpbmcgY3VybCAoYnlwYXNzZXMgQ2xvdWRmbGFyZSBicm93c2VyIGNoZWNrKS4iIiIKICAgIHRva2VuID0gX2dldF90b2tlbigpCiAgICB1cmwgPSBmIntCQVNFX1VSTH17cGF0aH0iCiAgICBpZiBwYXJhbXM6CiAgICAgICAgcXVlcnkgPSAiJiIuam9pbigKICAgICAgICAgICAgZiJ7a309e3VybGxpYi5wYXJzZS5xdW90ZShzdHIodikpfSIKICAgICAgICAgICAgZm9yIGssIHYgaW4gcGFyYW1zLml0ZW1zKCkgaWYgdiBpcyBub3QgTm9uZQogICAgICAgICkKICAgICAgICBpZiBxdWVyeToKICAgICAgICAgICAgdXJsID0gZiJ7dXJsfT97cXVlcnl9IgoKICAgIHJlc3VsdCA9IHN1YnByb2Nlc3MucnVuKAogICAgICAgIFsiY3VybCIsICItcyIsICItZiIsCiAgICAgICAgICItSCIsIGYiQXV0aG9yaXphdGlvbjogQmVhcmVyIHt0b2tlbn0iLAogICAgICAgICAiLUgiLCAiQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uIiwKICAgICAgICAgdXJsXSwKICAgICAgICBjYXB0dXJlX291dHB1dD1UcnVlLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MzAsCiAgICApCiAgICBpZiByZXN1bHQucmV0dXJuY29kZSAhPSAwOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigKICAgICAgICAgICAgZiJQbGF1ZCBBUEkgZXJyb3IgKGN1cmwgZXhpdCB7cmVzdWx0LnJldHVybmNvZGV9KToge3Jlc3VsdC5zdGRlcnJbOjMwMF19IgogICAgICAgICkKICAgIHRyeToKICAgICAgICByZXR1cm4ganNvbi5sb2FkcyhyZXN1bHQuc3Rkb3V0KQogICAgZXhjZXB0IGpzb24uSlNPTkRlY29kZUVycm9yOgogICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIkludmFsaWQgSlNPTiBmcm9tIFBsYXVkIEFQSToge3Jlc3VsdC5zdGRvdXRbOjMwMF19IikKCgpkZWYgX2ZldGNoX3MzX2pzb24odXJsOiBzdHIpIC0+IG9iamVjdDoKICAgICIiIkZldGNoIGFuZCBkZWNvbXByZXNzIGEgZ3ppcHBlZCBKU09OIGZyb20gYSBzaWduZWQgUzMgVVJMLiIiIgogICAgcmVzdWx0ID0gc3VicHJvY2Vzcy5ydW4oCiAgICAgICAgWyJjdXJsIiwgIi1zIiwgIi1mIiwgdXJsXSwKICAgICAgICBjYXB0dXJlX291dHB1dD1UcnVlLCB0aW1lb3V0PTMwLAogICAgKQogICAgaWYgcmVzdWx0LnJldHVybmNvZGUgIT0gMDoKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJTMyBmZXRjaCBmYWlsZWQgKGN1cmwgZXhpdCB7cmVzdWx0LnJldHVybmNvZGV9KSIpCiAgICB0cnk6CiAgICAgICAgY29udGVudCA9IGd6aXAuZGVjb21wcmVzcyhyZXN1bHQuc3Rkb3V0KQogICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgY29udGVudCA9IHJlc3VsdC5zdGRvdXQgICMgTm90IGd6aXBwZWQKICAgIHJldHVybiBqc29uLmxvYWRzKGNvbnRlbnQuZGVjb2RlKCJ1dGYtOCIpKQoKCiMg4pSA4pSAIERhdGEgaGVscGVycyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCmRlZiBfZm10X2R1cmF0aW9uKG1zOiBpbnQpIC0+IHN0cjoKICAgIG1pbnMgPSBtcyAvLyA2MDAwMAogICAgaWYgbWlucyA8IDYwOgogICAgICAgIHJldHVybiBmInttaW5zfW0iCiAgICByZXR1cm4gZiJ7bWlucyAvLyA2MH1oIHttaW5zICUgNjB9bSIKCgpkZWYgX2ZtdF9kYXRlKHRzX21zOiBpbnQpIC0+IHN0cjoKICAgIHRyeToKICAgICAgICBkdCA9IGRhdGV0aW1lLmZyb210aW1lc3RhbXAodHNfbXMgLyAxMDAwLCB0ej10aW1lem9uZS51dGMpCiAgICAgICAgcmV0dXJuIGR0LnN0cmZ0aW1lKCIlWS0lbS0lZCAlSDolTSBVVEMiKQogICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICByZXR1cm4gc3RyKHRzX21zKQoKCmRlZiBfc3VtbWFyaXNlX3JlY29yZGluZyhyOiBkaWN0KSAtPiBkaWN0OgogICAgIiIiUmV0dXJuIGEgY29tcGFjdCBkaWN0IHN1aXRhYmxlIGZvciBsaXN0aW5nLiIiIgogICAgcmV0dXJuIHsKICAgICAgICAiaWQiOiByLmdldCgiaWQiLCAiIiksCiAgICAgICAgInRpdGxlIjogci5nZXQoImZpbGVuYW1lIikgb3Igci5nZXQoImZpbGVfbmFtZSIpIG9yIHIuZ2V0KCJ0aXRsZSIpIG9yICIodW50aXRsZWQpIiwKICAgICAgICAiZGF0ZSI6IF9mbXRfZGF0ZShyLmdldCgic3RhcnRfdGltZSIsIDApKSwKICAgICAgICAiZHVyYXRpb24iOiBfZm10X2R1cmF0aW9uKHIuZ2V0KCJkdXJhdGlvbiIsIDApKSwKICAgICAgICAiaGFzX3RyYW5zY3JpcHQiOiBib29sKHIuZ2V0KCJpc190cmFucyIpKSwKICAgICAgICAiaGFzX3N1bW1hcnkiOiBib29sKHIuZ2V0KCJhdXRvX3N1bV9ub3RlIikgb3Igci5nZXQoImlzX3N1bSIpKSwKICAgIH0KCgpkZWYgX2Zvcm1hdF90cmFuc2NyaXB0KHNlZ21lbnRzOiBsaXN0KSAtPiBzdHI6CiAgICAiIiJGb3JtYXQgdHJhbnNjcmlwdCBzZWdtZW50cyBpbnRvIHJlYWRhYmxlIHRleHQuIiIiCiAgICBsaW5lcyA9IFtdCiAgICBjdXJyZW50X3NwZWFrZXIgPSBOb25lCiAgICBmb3Igc2VnIGluIHNlZ21lbnRzOgogICAgICAgIHNwZWFrZXIgPSBzZWcuZ2V0KCJzcGVha2VyIikgb3Igc2VnLmdldCgib3JpZ2luYWxfc3BlYWtlciIpIG9yICIiCiAgICAgICAgY29udGVudCA9IHNlZy5nZXQoImNvbnRlbnQiLCAiIikuc3RyaXAoKQogICAgICAgIGlmIG5vdCBjb250ZW50OgogICAgICAgICAgICBjb250aW51ZQogICAgICAgIGlmIHNwZWFrZXIgYW5kIHNwZWFrZXIgIT0gY3VycmVudF9zcGVha2VyOgogICAgICAgICAgICBpZiBsaW5lczoKICAgICAgICAgICAgICAgIGxpbmVzLmFwcGVuZCgiIikKICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiW3tzcGVha2VyfV0iKQogICAgICAgICAgICBjdXJyZW50X3NwZWFrZXIgPSBzcGVha2VyCiAgICAgICAgbGluZXMuYXBwZW5kKGNvbnRlbnQpCiAgICByZXR1cm4gIlxuIi5qb2luKGxpbmVzKQoKCmRlZiBfZ2V0X2NvbnRlbnRfdXJsKGNvbnRlbnRfbGlzdDogbGlzdCwgZGF0YV90eXBlOiBzdHIpIC0+IE9wdGlvbmFsW3N0cl06CiAgICAiIiJGaW5kIGEgZGF0YV9saW5rIFVSTCBmcm9tIGNvbnRlbnRfbGlzdCBieSBkYXRhX3R5cGUuIiIiCiAgICBmb3IgaXRlbSBpbiBjb250ZW50X2xpc3Q6CiAgICAgICAgaWYgaXRlbS5nZXQoImRhdGFfdHlwZSIpID09IGRhdGFfdHlwZSBhbmQgaXRlbS5nZXQoImRhdGFfbGluayIpOgogICAgICAgICAgICByZXR1cm4gaXRlbVsiZGF0YV9saW5rIl0KICAgIHJldHVybiBOb25lCgoKIyDilIDilIAgTUNQIHNlcnZlciDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKbWNwID0gRmFzdE1DUCgicGxhdWQtbXBtIikKCgpAbWNwLnRvb2woKQpkZWYgcGxhdWRfbGlzdF9yZWNvcmRpbmdzKAogICAgbGltaXQ6IGludCA9IDUwLAogICAgb25seV93aXRoX3RyYW5zY3JpcHQ6IGJvb2wgPSBGYWxzZSwKKSAtPiBzdHI6CiAgICAiIiIKICAgIExpc3QgUGxhdWQgcmVjb3JkaW5ncyBuZXdlc3QtZmlyc3QuCgogICAgQXJnczoKICAgICAgICBsaW1pdDogTWF4IG51bWJlciB0byByZXR1cm4gKGRlZmF1bHQgNTAsIG1heCAyMDApLgogICAgICAgIG9ubHlfd2l0aF90cmFuc2NyaXB0OiBJZiB0cnVlLCBvbmx5IHJldHVybiByZWNvcmRpbmdzIHRoYXQgaGF2ZSBhIHRyYW5zY3JpcHQuCiAgICAiIiIKICAgIHJlc3AgPSBfYXBpX2dldCgiL2ZpbGUvc2ltcGxlL3dlYiIpCiAgICBpZiBpc2luc3RhbmNlKHJlc3AsIGRpY3QpOgogICAgICAgIHJlY29yZGluZ3MgPSByZXNwLmdldCgiZGF0YV9maWxlX2xpc3QiKSBvciByZXNwLmdldCgiZGF0YSIpIG9yIFtdCiAgICBlbHNlOgogICAgICAgIHJlY29yZGluZ3MgPSByZXNwIGlmIGlzaW5zdGFuY2UocmVzcCwgbGlzdCkgZWxzZSBbXQoKICAgIGlmIG9ubHlfd2l0aF90cmFuc2NyaXB0OgogICAgICAgIHJlY29yZGluZ3MgPSBbciBmb3IgciBpbiByZWNvcmRpbmdzIGlmIHIuZ2V0KCJpc190cmFucyIpXQoKICAgIHJlY29yZGluZ3MgPSByZWNvcmRpbmdzWzptaW4obGltaXQsIDIwMCldCiAgICByZXR1cm4ganNvbi5kdW1wcyhbX3N1bW1hcmlzZV9yZWNvcmRpbmcocikgZm9yIHIgaW4gcmVjb3JkaW5nc10sIGluZGVudD0yKQoKCkBtY3AudG9vbCgpCmRlZiBwbGF1ZF9zZWFyY2hfcmVjb3JkaW5ncygKICAgIHRpdGxlX2NvbnRhaW5zOiBPcHRpb25hbFtzdHJdID0gTm9uZSwKICAgIHN0YXJ0X2RhdGU6IE9wdGlvbmFsW3N0cl0gPSBOb25lLAogICAgZW5kX2RhdGU6IE9wdGlvbmFsW3N0cl0gPSBOb25lLAogICAgb25seV93aXRoX3RyYW5zY3JpcHQ6IGJvb2wgPSBGYWxzZSwKICAgIG1heF9yZXN1bHRzOiBpbnQgPSAyMCwKKSAtPiBzdHI6CiAgICAiIiIKICAgIFNlYXJjaCByZWNvcmRpbmdzIGJ5IHRpdGxlIGFuZC9vciBkYXRlIHJhbmdlLgoKICAgIEFyZ3M6CiAgICAgICAgdGl0bGVfY29udGFpbnM6IFN1YnN0cmluZyB0byBtYXRjaCBpbiB0aGUgcmVjb3JkaW5nIHRpdGxlIChjYXNlLWluc2Vuc2l0aXZlKS4KICAgICAgICBzdGFydF9kYXRlOiBJU08gZGF0ZSBzdHJpbmcsIGUuZy4gJzIwMjYtMDQtMDEnLiBPbmx5IHJlY29yZGluZ3Mgb24gb3IgYWZ0ZXIgdGhpcyBkYXRlLgogICAgICAgIGVuZF9kYXRlOiBJU08gZGF0ZSBzdHJpbmcsIGUuZy4gJzIwMjYtMDUtMDEnLiBPbmx5IHJlY29yZGluZ3Mgb24gb3IgYmVmb3JlIHRoaXMgZGF0ZS4KICAgICAgICBvbmx5X3dpdGhfdHJhbnNjcmlwdDogSWYgdHJ1ZSwgb25seSByZXR1cm4gcmVjb3JkaW5ncyB0aGF0IGhhdmUgYSB0cmFuc2NyaXB0LgogICAgICAgIG1heF9yZXN1bHRzOiBNYXggbnVtYmVyIG9mIHJlc3VsdHMgdG8gcmV0dXJuIChkZWZhdWx0IDIwKS4KICAgICIiIgogICAgcmVzcCA9IF9hcGlfZ2V0KCIvZmlsZS9zaW1wbGUvd2ViIikKICAgIGlmIGlzaW5zdGFuY2UocmVzcCwgZGljdCk6CiAgICAgICAgcmVjb3JkaW5ncyA9IHJlc3AuZ2V0KCJkYXRhX2ZpbGVfbGlzdCIpIG9yIHJlc3AuZ2V0KCJkYXRhIikgb3IgW10KICAgIGVsc2U6CiAgICAgICAgcmVjb3JkaW5ncyA9IHJlc3AgaWYgaXNpbnN0YW5jZShyZXNwLCBsaXN0KSBlbHNlIFtdCgogICAgZGVmIG1hdGNoZXMocjogZGljdCkgLT4gYm9vbDoKICAgICAgICBpZiBvbmx5X3dpdGhfdHJhbnNjcmlwdCBhbmQgbm90IHIuZ2V0KCJpc190cmFucyIpOgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB0aXRsZSA9IChyLmdldCgiZmlsZW5hbWUiKSBvciByLmdldCgiZmlsZV9uYW1lIikgb3Igci5nZXQoInRpdGxlIikgb3IgIiIpLmxvd2VyKCkKICAgICAgICBpZiB0aXRsZV9jb250YWlucyBhbmQgdGl0bGVfY29udGFpbnMubG93ZXIoKSBub3QgaW4gdGl0bGU6CiAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgICAgIGlmIHN0YXJ0X2RhdGUgb3IgZW5kX2RhdGU6CiAgICAgICAgICAgIHRzX21zID0gci5nZXQoInN0YXJ0X3RpbWUiLCAwKQogICAgICAgICAgICByZWNfZGF0ZSA9IGRhdGV0aW1lLmZyb210aW1lc3RhbXAodHNfbXMgLyAxMDAwLCB0ej10aW1lem9uZS51dGMpLmRhdGUoKQogICAgICAgICAgICBpZiBzdGFydF9kYXRlIGFuZCByZWNfZGF0ZSA8IGRhdGV0aW1lLmZyb21pc29mb3JtYXQoc3RhcnRfZGF0ZSkuZGF0ZSgpOgogICAgICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgICAgIGlmIGVuZF9kYXRlIGFuZCByZWNfZGF0ZSA+IGRhdGV0aW1lLmZyb21pc29mb3JtYXQoZW5kX2RhdGUpLmRhdGUoKToKICAgICAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgICAgIHJldHVybiBUcnVlCgogICAgbWF0Y2hlZCA9IFtyIGZvciByIGluIHJlY29yZGluZ3MgaWYgbWF0Y2hlcyhyKV1bOm1heF9yZXN1bHRzXQogICAgcmV0dXJuIGpzb24uZHVtcHMoW19zdW1tYXJpc2VfcmVjb3JkaW5nKHIpIGZvciByIGluIG1hdGNoZWRdLCBpbmRlbnQ9MikKCgpAbWNwLnRvb2woKQpkZWYgcGxhdWRfZ2V0X3RyYW5zY3JpcHQocmVjb3JkaW5nX2lkOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgR2V0IHRoZSBmdWxsIHRyYW5zY3JpcHQgZm9yIGEgUGxhdWQgcmVjb3JkaW5nLgoKICAgIEFyZ3M6CiAgICAgICAgcmVjb3JkaW5nX2lkOiBUaGUgcmVjb3JkaW5nIElEIChmcm9tIHBsYXVkX2xpc3RfcmVjb3JkaW5ncykuCiAgICAiIiIKICAgIHJlc3AgPSBfYXBpX2dldChmIi9maWxlL2RldGFpbC97cmVjb3JkaW5nX2lkfSIpCiAgICBkYXRhID0gcmVzcC5nZXQoImRhdGEiLCByZXNwKSBpZiBpc2luc3RhbmNlKHJlc3AsIGRpY3QpIGVsc2UgcmVzcAoKICAgIHRpdGxlID0gZGF0YS5nZXQoImZpbGVfbmFtZSIpIG9yIGRhdGEuZ2V0KCJmaWxlbmFtZSIpIG9yIGRhdGEuZ2V0KCJ0aXRsZSIpIG9yICIodW50aXRsZWQpIgogICAgY29udGVudF9saXN0ID0gZGF0YS5nZXQoImNvbnRlbnRfbGlzdCIpIG9yIFtdCiAgICB0cmFuc2NyaXB0ID0gIiIKCiAgICAjIFRyeSBwb2xpc2hlZCB0cmFuc2NyaXB0IGZpcnN0LCBmYWxsIGJhY2sgdG8gcmF3CiAgICBmb3IgZHR5cGUgaW4gKCJ0cmFuc2FjdGlvbl9wb2xpc2giLCAidHJhbnNhY3Rpb24iKToKICAgICAgICB1cmwgPSBfZ2V0X2NvbnRlbnRfdXJsKGNvbnRlbnRfbGlzdCwgZHR5cGUpCiAgICAgICAgaWYgdXJsOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBzZWdtZW50cyA9IF9mZXRjaF9zM19qc29uKHVybCkKICAgICAgICAgICAgICAgIGlmIGlzaW5zdGFuY2Uoc2VnbWVudHMsIGxpc3QpOgogICAgICAgICAgICAgICAgICAgIHRyYW5zY3JpcHQgPSBfZm9ybWF0X3RyYW5zY3JpcHQoc2VnbWVudHMpCiAgICAgICAgICAgICAgICAgICAgaWYgdHJhbnNjcmlwdDoKICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICAgICAgdHJhbnNjcmlwdCA9IGYiRXJyb3IgZmV0Y2hpbmcgdHJhbnNjcmlwdDoge2V9IgogICAgICAgICAgICAgICAgYnJlYWsKCiAgICByZXR1cm4ganNvbi5kdW1wcyh7CiAgICAgICAgImlkIjogcmVjb3JkaW5nX2lkLAogICAgICAgICJ0aXRsZSI6IHRpdGxlLAogICAgICAgICJkYXRlIjogX2ZtdF9kYXRlKGRhdGEuZ2V0KCJzdGFydF90aW1lIiwgMCkpLAogICAgICAgICJkdXJhdGlvbiI6IF9mbXRfZHVyYXRpb24oZGF0YS5nZXQoImR1cmF0aW9uIiwgMCkpLAogICAgICAgICJ0cmFuc2NyaXB0IjogdHJhbnNjcmlwdCBvciAiTm8gdHJhbnNjcmlwdCBhdmFpbGFibGUgZm9yIHRoaXMgcmVjb3JkaW5nLiIsCiAgICB9LCBpbmRlbnQ9MikKCgpAbWNwLnRvb2woKQpkZWYgcGxhdWRfZ2V0X3N1bW1hcnkocmVjb3JkaW5nX2lkOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgR2V0IHRoZSBBSS1nZW5lcmF0ZWQgc3VtbWFyeS9vdXRsaW5lIGZvciBhIFBsYXVkIHJlY29yZGluZy4KCiAgICBBcmdzOgogICAgICAgIHJlY29yZGluZ19pZDogVGhlIHJlY29yZGluZyBJRCAoZnJvbSBwbGF1ZF9saXN0X3JlY29yZGluZ3MpLgogICAgIiIiCiAgICByZXNwID0gX2FwaV9nZXQoZiIvZmlsZS9kZXRhaWwve3JlY29yZGluZ19pZH0iKQogICAgZGF0YSA9IHJlc3AuZ2V0KCJkYXRhIiwgcmVzcCkgaWYgaXNpbnN0YW5jZShyZXNwLCBkaWN0KSBlbHNlIHJlc3AKCiAgICB0aXRsZSA9IGRhdGEuZ2V0KCJmaWxlX25hbWUiKSBvciBkYXRhLmdldCgiZmlsZW5hbWUiKSBvciBkYXRhLmdldCgidGl0bGUiKSBvciAiKHVudGl0bGVkKSIKICAgIGNvbnRlbnRfbGlzdCA9IGRhdGEuZ2V0KCJjb250ZW50X2xpc3QiKSBvciBbXQogICAgc3VtbWFyeSA9ICIiCgogICAgdXJsID0gX2dldF9jb250ZW50X3VybChjb250ZW50X2xpc3QsICJvdXRsaW5lIikKICAgIGlmIHVybDoKICAgICAgICB0cnk6CiAgICAgICAgICAgIG91dGxpbmVfZGF0YSA9IF9mZXRjaF9zM19qc29uKHVybCkKICAgICAgICAgICAgaWYgaXNpbnN0YW5jZShvdXRsaW5lX2RhdGEsIGRpY3QpOgogICAgICAgICAgICAgICAgc3VtbWFyeSA9IGpzb24uZHVtcHMob3V0bGluZV9kYXRhLCBpbmRlbnQ9MikKICAgICAgICAgICAgZWxpZiBpc2luc3RhbmNlKG91dGxpbmVfZGF0YSwgbGlzdCk6CiAgICAgICAgICAgICAgICBwYXJ0cyA9IFtdCiAgICAgICAgICAgICAgICBmb3IgaXRlbSBpbiBvdXRsaW5lX2RhdGE6CiAgICAgICAgICAgICAgICAgICAgaWYgaXNpbnN0YW5jZShpdGVtLCBkaWN0KToKICAgICAgICAgICAgICAgICAgICAgICAgcGFydHMuYXBwZW5kKGl0ZW0uZ2V0KCJjb250ZW50Iikgb3IgaXRlbS5nZXQoInRleHQiKSBvciBzdHIoaXRlbSkpCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgcGFydHMuYXBwZW5kKHN0cihpdGVtKSkKICAgICAgICAgICAgICAgIHN1bW1hcnkgPSAiXG5cbiIuam9pbihwIGZvciBwIGluIHBhcnRzIGlmIHApCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBzdW1tYXJ5ID0gc3RyKG91dGxpbmVfZGF0YSkKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIHN1bW1hcnkgPSBmIkVycm9yIGZldGNoaW5nIHN1bW1hcnk6IHtlfSIKCiAgICByZXR1cm4ganNvbi5kdW1wcyh7CiAgICAgICAgImlkIjogcmVjb3JkaW5nX2lkLAogICAgICAgICJ0aXRsZSI6IHRpdGxlLAogICAgICAgICJkYXRlIjogX2ZtdF9kYXRlKGRhdGEuZ2V0KCJzdGFydF90aW1lIiwgMCkpLAogICAgICAgICJzdW1tYXJ5Ijogc3VtbWFyeSBvciAiTm8gc3VtbWFyeSBhdmFpbGFibGUgZm9yIHRoaXMgcmVjb3JkaW5nLiIsCiAgICB9LCBpbmRlbnQ9MikKCgpAbWNwLnRvb2woKQpkZWYgcGxhdWRfZ2V0X25vdGVzKHJlY29yZGluZ19pZDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIEdldCBBSS1nZW5lcmF0ZWQgbm90ZXMgYW5kIGFjdGlvbiBpdGVtcyBmb3IgYSBQbGF1ZCByZWNvcmRpbmcuCiAgICBSZXR1cm5zIGFsbCBub3RlIGVudHJpZXMgZnJvbSB0aGUgY29udGVudF9saXN0IChhY3Rpb24gaXRlbXMsIGtleSBwb2ludHMsIGV0Yy4pLgoKICAgIEFyZ3M6CiAgICAgICAgcmVjb3JkaW5nX2lkOiBUaGUgcmVjb3JkaW5nIElEIChmcm9tIHBsYXVkX2xpc3RfcmVjb3JkaW5ncykuCiAgICAiIiIKICAgIHJlc3AgPSBfYXBpX2dldChmIi9maWxlL2RldGFpbC97cmVjb3JkaW5nX2lkfSIpCiAgICBkYXRhID0gcmVzcC5nZXQoImRhdGEiLCByZXNwKSBpZiBpc2luc3RhbmNlKHJlc3AsIGRpY3QpIGVsc2UgcmVzcAoKICAgIHRpdGxlID0gZGF0YS5nZXQoImZpbGVfbmFtZSIpIG9yIGRhdGEuZ2V0KCJmaWxlbmFtZSIpIG9yIGRhdGEuZ2V0KCJ0aXRsZSIpIG9yICIodW50aXRsZWQpIgogICAgY29udGVudF9saXN0ID0gZGF0YS5nZXQoImNvbnRlbnRfbGlzdCIpIG9yIFtdCiAgICBub3RlcyA9IFtdCgogICAgZm9yIGl0ZW0gaW4gY29udGVudF9saXN0OgogICAgICAgIGR0eXBlID0gaXRlbS5nZXQoImRhdGFfdHlwZSIsICIiKQogICAgICAgIHVybCA9IGl0ZW0uZ2V0KCJkYXRhX2xpbmsiKQogICAgICAgIGlmIG5vdCB1cmwgb3IgZHR5cGUgaW4gKCJ0cmFuc2FjdGlvbiIsICJ0cmFuc2FjdGlvbl9wb2xpc2giKToKICAgICAgICAgICAgY29udGludWUgICMgU2tpcCByYXcgdHJhbnNjcmlwdCB0eXBlcwoKICAgICAgICBub3RlX3RpdGxlID0gaXRlbS5nZXQoImRhdGFfdGl0bGUiKSBvciBpdGVtLmdldCgiZGF0YV90YWJfbmFtZSIpIG9yIGR0eXBlCiAgICAgICAgdHJ5OgogICAgICAgICAgICBub3RlX2RhdGEgPSBfZmV0Y2hfczNfanNvbih1cmwpCiAgICAgICAgICAgIGlmIGlzaW5zdGFuY2Uobm90ZV9kYXRhLCBsaXN0KToKICAgICAgICAgICAgICAgIGNvbnRlbnQgPSAiXG4iLmpvaW4oCiAgICAgICAgICAgICAgICAgICAgc3RyKHNlZy5nZXQoImNvbnRlbnQiKSBvciBzZWcuZ2V0KCJ0ZXh0Iikgb3Igc2VnKQogICAgICAgICAgICAgICAgICAgIGZvciBzZWcgaW4gbm90ZV9kYXRhIGlmIHNlZwogICAgICAgICAgICAgICAgKQogICAgICAgICAgICBlbGlmIGlzaW5zdGFuY2Uobm90ZV9kYXRhLCBkaWN0KToKICAgICAgICAgICAgICAgIGNvbnRlbnQgPSBqc29uLmR1bXBzKG5vdGVfZGF0YSwgaW5kZW50PTIpCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBjb250ZW50ID0gc3RyKG5vdGVfZGF0YSkKCiAgICAgICAgICAgIGlmIGNvbnRlbnQuc3RyaXAoKToKICAgICAgICAgICAgICAgIG5vdGVzLmFwcGVuZCh7InR5cGUiOiBkdHlwZSwgInRpdGxlIjogbm90ZV90aXRsZSwgImNvbnRlbnQiOiBjb250ZW50fSkKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIG5vdGVzLmFwcGVuZCh7InR5cGUiOiBkdHlwZSwgInRpdGxlIjogbm90ZV90aXRsZSwgImNvbnRlbnQiOiBmIkVycm9yOiB7ZX0ifSkKCiAgICByZXR1cm4ganNvbi5kdW1wcyh7CiAgICAgICAgImlkIjogcmVjb3JkaW5nX2lkLAogICAgICAgICJ0aXRsZSI6IHRpdGxlLAogICAgICAgICJkYXRlIjogX2ZtdF9kYXRlKGRhdGEuZ2V0KCJzdGFydF90aW1lIiwgMCkpLAogICAgICAgICJub3RlcyI6IG5vdGVzIGlmIG5vdGVzIGVsc2UgW3sidHlwZSI6ICJub25lIiwgImNvbnRlbnQiOiAiTm8gbm90ZXMgYXZhaWxhYmxlIGZvciB0aGlzIHJlY29yZGluZy4ifV0sCiAgICB9LCBpbmRlbnQ9MikKCgpAbWNwLnRvb2woKQpkZWYgcGxhdWRfZ2V0X3JlY29yZGluZ19kZXRhaWwocmVjb3JkaW5nX2lkOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgR2V0IGZ1bGwgbWV0YWRhdGEgZm9yIGEgUGxhdWQgcmVjb3JkaW5nIGluY2x1ZGluZyB0aXRsZSwgZGF0ZSwgZHVyYXRpb24sCiAgICB0cmFuc2NyaXB0IGF2YWlsYWJpbGl0eSwgYW5kIGF2YWlsYWJsZSBjb250ZW50IHR5cGVzLgoKICAgIEFyZ3M6CiAgICAgICAgcmVjb3JkaW5nX2lkOiBUaGUgcmVjb3JkaW5nIElEIChmcm9tIHBsYXVkX2xpc3RfcmVjb3JkaW5ncykuCiAgICAiIiIKICAgIHJlc3AgPSBfYXBpX2dldChmIi9maWxlL2RldGFpbC97cmVjb3JkaW5nX2lkfSIpCiAgICBkYXRhID0gcmVzcC5nZXQoImRhdGEiLCByZXNwKSBpZiBpc2luc3RhbmNlKHJlc3AsIGRpY3QpIGVsc2UgcmVzcAoKICAgIGNvbnRlbnRfbGlzdCA9IGRhdGEuZ2V0KCJjb250ZW50X2xpc3QiKSBvciBbXQogICAgY29udGVudF90eXBlcyA9IFtpdGVtLmdldCgiZGF0YV90eXBlIikgZm9yIGl0ZW0gaW4gY29udGVudF9saXN0IGlmIGl0ZW0uZ2V0KCJkYXRhX3R5cGUiKV0KCiAgICByZXR1cm4ganNvbi5kdW1wcyh7CiAgICAgICAgImlkIjogcmVjb3JkaW5nX2lkLAogICAgICAgICJ0aXRsZSI6IGRhdGEuZ2V0KCJmaWxlX25hbWUiKSBvciBkYXRhLmdldCgiZmlsZW5hbWUiKSBvciBkYXRhLmdldCgidGl0bGUiKSBvciAiKHVudGl0bGVkKSIsCiAgICAgICAgImRhdGUiOiBfZm10X2RhdGUoZGF0YS5nZXQoInN0YXJ0X3RpbWUiLCAwKSksCiAgICAgICAgImR1cmF0aW9uIjogX2ZtdF9kdXJhdGlvbihkYXRhLmdldCgiZHVyYXRpb24iLCAwKSksCiAgICAgICAgImhhc190cmFuc2NyaXB0IjogInRyYW5zYWN0aW9uIiBpbiBjb250ZW50X3R5cGVzIG9yICJ0cmFuc2FjdGlvbl9wb2xpc2giIGluIGNvbnRlbnRfdHlwZXMsCiAgICAgICAgImhhc19zdW1tYXJ5IjogIm91dGxpbmUiIGluIGNvbnRlbnRfdHlwZXMsCiAgICAgICAgImNvbnRlbnRfdHlwZXMiOiBjb250ZW50X3R5cGVzLAogICAgICAgICJkZXZpY2UiOiBkYXRhLmdldCgic2VyaWFsX251bWJlciIpIG9yIGRhdGEuZ2V0KCJkZXZpY2VfbmFtZSIpIG9yICJ1bmtub3duIiwKICAgICAgICAidGFncyI6IGRhdGEuZ2V0KCJmaWxldGFnX2lkX2xpc3QiKSBvciBkYXRhLmdldCgiZmlsZXRhZyIpIG9yIFtdLAogICAgfSwgaW5kZW50PTIpCgoKQG1jcC50b29sKCkKZGVmIHBsYXVkX3VzZXJfaW5mbygpIC0+IHN0cjoKICAgICIiIkdldCB0aGUgY3VycmVudCBQbGF1ZCBhY2NvdW50IGluZm9ybWF0aW9uIGFuZCBjb25uZWN0aW9uIHN0YXR1cy4iIiIKICAgIHJldHVybiBqc29uLmR1bXBzKHsKICAgICAgICAicmVnaW9uIjogUExBVURfUkVHSU9OLAogICAgICAgICJhcGlfYmFzZSI6IEJBU0VfVVJMLAogICAgICAgICJ0b2tlbl9sb2FkZWQiOiBib29sKF9sb2FkX3Rva2VuKCkpLAogICAgfSwgaW5kZW50PTIpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1jcC5ydW4oKQo= \ No newline at end of file