v0.1.1: Rewrite with curl-based HTTP (Cloudflare bypass), S3 gzip content fetching, correct API field names
- Switch urllib to curl subprocess to bypass Cloudflare Error 1010 - Add _fetch_s3_json() for gzipped content from signed S3 URLs - Fix list endpoint key: data_file_list (not data) - Fix detail endpoint title field: file_name (not filename) - Fix token key: support both token and accessToken in config - Add _format_transcript() with speaker grouping - plaud_get_transcript: fetch from content_list S3 URLs (transaction_polish → transaction) - plaud_get_summary: fetch outline from S3 - plaud_get_notes: fetch all non-transcript content_list items from S3 - plaud_get_recording_detail: derive has_transcript from content_types list
This commit is contained in:
1 parent
5cf373da97
commit
82b700e2e1
1 file changed
+1
-338
+1
-338
@@ -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 \"<jwt>\"\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+IG9iamVjdDoKICAgICIiIkZldGNoIGFuZCBkZWNvbXByZXNzIGEgZ3ppcHBlZCBKU09OIGZyb20gYSBzaWduZWQgUzMgVVJMLiIiIgogICAgcmVzdWx0ID0gc3VicHJvY2Vzcy5ydW4oCiAgICAgICAgWyJjdXJsIiwgIi1zIiwgIi1mIiwgdXJsXSwKICAgICAgICBjYXB0dXJlX291dHB1dD1UcnVlLCB0aW1lb3V0PTMwLAogICAgKQogICAgaWYgcmVzdWx0LnJldHVybmNvZGUgIT0gMDoKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJTMyBmZXRjaCBmYWlsZWQgKGN1cmwgZXhpdCB7cmVzdWx0LnJldHVybmNvZGV9KSIpCiAgICB0cnk6CiAgICAgICAgY29udGVudCA9IGd6aXAuZGVjb21wcmVzcyhyZXN1bHQuc3Rkb3V0KQogICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgY29udGVudCA9IHJlc3VsdC5zdGRvdXQgICMgTm90IGd6aXBwZWQKICAgIHJldHVybiBqc29uLmxvYWRzKGNvbnRlbnQuZGVjb2RlKCJ1dGYtOCIpKQoKCiMg4pSA4pSAIERhdGEgaGVscGVycyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDLine truncated
|
||||
Reference in new issue
Block a user