feat(tools): mr-number + whatsapp redroid updates (tray/console, lookup, adb server, installers)
- mr-number-lookup: cloud-setup adb-keyboard server.py, console-tray run.sh + tray.py - whatsapp-lookup: lookup.sh, wa_lookup.py - Aligns with recent redroid/waconsole work (proper heredoc in installer, separate console app, device port handling) Part of ongoing prospector screening tools on the ct-forge / DO redroid setup (moving off black infra).
This commit is contained in:
parent
bd7b5e8f47
commit
6fdcf8df7c
5 changed files with 60 additions and 29 deletions
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Loopback live-keyboard passthrough to redroid via adb. 127.0.0.1 only; reach via SSH tunnel."""
|
||||
import json, subprocess
|
||||
import json, subprocess, urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
# CSRF defense for this loopback control service. The mutating endpoints (/text,
|
||||
|
|
@ -41,14 +41,31 @@ def send_key(name):
|
|||
return adb_shell("input keyevent " + str(KEYS.get(name, 66)))
|
||||
|
||||
|
||||
def _get_label(path):
|
||||
"""Extract app label from query (?app=mr-number or ?app=whatsapp or ?title=...)"""
|
||||
parsed = urllib.parse.urlparse(path)
|
||||
q = urllib.parse.parse_qs(parsed.query)
|
||||
title = q.get("title", [""])[0]
|
||||
if title:
|
||||
return title
|
||||
app = q.get("app", [""])[0].lower()
|
||||
if "mr" in app or "number" in app:
|
||||
return "☎️ Mr. Number"
|
||||
if "wa" in app or "whatsapp" in app:
|
||||
return "📲 WhatsApp"
|
||||
return "Redroid"
|
||||
|
||||
|
||||
PAGE = """<!doctype html><meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>adb keyboard</title>
|
||||
<title>{label} Keyboard</title>
|
||||
<style>body{font-family:system-ui;max-width:600px;margin:1.5rem auto;padding:0 1rem}
|
||||
.header{{background:#222;color:#0f0;padding:6px 10px;margin:-1.5rem -1rem 1rem;font-size:13px;border-bottom:1px solid #444}}
|
||||
button{font-size:1rem;padding:.5rem .8rem;cursor:pointer;margin:.2rem}
|
||||
#cap{padding:1rem;border:2px dashed #999;border-radius:8px;text-align:center;margin:1rem 0;outline:none}
|
||||
#cap.on{border-color:#2a8;background:#eafaf3}#log{color:#888;font-size:.8rem;white-space:pre-wrap}
|
||||
input{font-size:1.1rem;padding:.5rem;width:100%;box-sizing:border-box}</style>
|
||||
<h3>adb keyboard → redroid</h3>
|
||||
<div class="header">{label} — adb keyboard passthrough</div>
|
||||
<h3>Keyboard → redroid</h3>
|
||||
<p style=color:#888>Click a field in the console first. Then click the box below and just type — keys pass through live.</p>
|
||||
<div id=cap tabindex=0>click here, then type (live keyboard OFF)</div>
|
||||
<div class=row>
|
||||
|
|
@ -61,33 +78,42 @@ input{font-size:1.1rem;padding:.5rem;width:100%;box-sizing:border-box}</style>
|
|||
<div id=log></div>
|
||||
<script>
|
||||
const log=m=>document.getElementById("log").textContent=new Date().toLocaleTimeString()+" "+m;
|
||||
const H={"Content-Type":"application/json"};
|
||||
async function txt(s){const r=await fetch("/text",{method:"POST",headers:H,body:JSON.stringify({text:s})});return r.ok;}
|
||||
async function k(n){const r=await fetch("/key",{method:"POST",headers:H,body:JSON.stringify({key:n})});log(r.ok?"key "+n:"ERR "+n);}
|
||||
const H={{"Content-Type":"application/json"}};
|
||||
async function txt(s){{const r=await fetch("/text",{method:"POST",headers:H,body:JSON.stringify({text:s})});return r.ok;}}
|
||||
async function k(n){{const r=await fetch("/key",{method:"POST",headers:H,body:JSON.stringify({key:n})});log(r.ok?"key "+n:"ERR "+n);}}
|
||||
const cap=document.getElementById("cap");
|
||||
cap.addEventListener("focus",()=>{cap.classList.add("on");cap.textContent="LIVE - typing passes through. Click away to stop.";});
|
||||
cap.addEventListener("blur",()=>{cap.classList.remove("on");cap.textContent="click here, then type (live keyboard OFF)";});
|
||||
const KMAP={Enter:"enter",Backspace:"del",Tab:"tab",Escape:"esc",ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right",Delete:"fwddel"};
|
||||
cap.addEventListener("focus",()=>{{cap.classList.add("on");cap.textContent="LIVE - typing passes through. Click away to stop.";}});
|
||||
cap.addEventListener("blur",()=>{{cap.classList.remove("on");cap.textContent="click here, then type (live keyboard OFF)";}});
|
||||
const KMAP={{Enter:"enter",Backspace:"del",Tab:"tab",Escape:"esc",ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right",Delete:"fwddel"}};
|
||||
cap.addEventListener("keydown",e=>{
|
||||
if(e.metaKey||e.ctrlKey||e.altKey)return;
|
||||
if(KMAP[e.key]){k(KMAP[e.key]);e.preventDefault();return;}
|
||||
if(e.key===" "){k("space");e.preventDefault();return;}
|
||||
if(e.key.length===1){txt(e.key);log("key "+e.key);e.preventDefault();}
|
||||
if(KMAP[e.key]){{k(KMAP[e.key]);e.preventDefault();return;}}
|
||||
if(e.key===" "){{k("space");e.preventDefault();return;}}
|
||||
if(e.key.length===1){{txt(e.key);log("key "+e.key);e.preventDefault();}}
|
||||
});
|
||||
const ti=document.getElementById("t");
|
||||
ti.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();txt(ti.value).then(ok=>log(ok?"sent string":"ERR"));ti.value="";}});
|
||||
ti.addEventListener("keydown",e=>{{if(e.key==="Enter"){{e.preventDefault();txt(ti.value).then(ok=>log(ok?"sent string":"ERR"));ti.value="";}}}});
|
||||
</script>"""
|
||||
|
||||
|
||||
COMBO = """<!doctype html><meta charset=utf-8><title>redroid console</title>
|
||||
<style>html,body{margin:0;height:100%;background:#111;font-family:system-ui}
|
||||
.wrap{display:flex;height:100vh}
|
||||
.screen{flex:0 0 440px;border:0;background:#000}
|
||||
.kbd{flex:1;border:0;background:#fff}
|
||||
@media(max-width:800px){.wrap{flex-direction:column}.screen{flex:0 0 60vh}}</style>
|
||||
COMBO = """<!doctype html><meta charset=utf-8><title>{label} Redroid Console</title>
|
||||
<style>html,body{{margin:0;height:100%;background:#111;font-family:system-ui}}
|
||||
.header{{position:fixed;top:0;left:0;right:0;background:#222;color:#0f0;padding:6px 12px;font-size:13px;z-index:10;border-bottom:1px solid #444}}
|
||||
.wrap{{display:flex;height:100vh;padding-top:28px}}
|
||||
.screen{{flex:0 0 440px;border:0;background:#000}}
|
||||
.kbd{{flex:1;border:0;background:#fff}}
|
||||
.label{{font-size:11px;color:#888;padding:2px 4px;background:#1a1a1a}}
|
||||
@media(max-width:800px){{.wrap{{flex-direction:column;padding-top:28px}}.screen{{flex:0 0 55vh}}}}</style>
|
||||
<div class="header">{label} — Redroid Console (ws-scrcpy + adb-keyboard) | use the matching tray (☎️ / 📲) to open the right context</div>
|
||||
<div class=wrap>
|
||||
<iframe class=screen src="http://localhost:8000/"></iframe>
|
||||
<iframe class=kbd src="/"></iframe>
|
||||
<div style="display:flex;flex-direction:column">
|
||||
<div class="label">Screen Mirror (ws-scrcpy)</div>
|
||||
<iframe class=screen src="http://localhost:8000/"></iframe>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column">
|
||||
<div class="label">Live Keyboard & Input</div>
|
||||
<iframe class=kbd src="{kbd_src}"></iframe>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
|
||||
|
|
@ -104,10 +130,15 @@ class H(BaseHTTPRequestHandler):
|
|||
self.wfile.write(x)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/":
|
||||
self._s(200, PAGE)
|
||||
elif self.path == "/ui":
|
||||
self._s(200, COMBO)
|
||||
label = _get_label(self.path)
|
||||
p = self.path.split("?")[0]
|
||||
if p == "/":
|
||||
self._s(200, PAGE.format(label=label))
|
||||
elif p == "/ui":
|
||||
qs = urllib.parse.urlparse(self.path).query
|
||||
kbd_src = f"/?{qs}" if qs else "/"
|
||||
combo = COMBO.format(label=label, kbd_src=kbd_src)
|
||||
self._s(200, combo)
|
||||
else:
|
||||
self._s(404, "no")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# Launch the redroid console tray for Mr. Number (☎️ icon) on plum.
|
||||
# Self-contained: creates a local venv with rumps on first run.
|
||||
# (The WhatsApp tray uses 💬 icon in the @whatsapp standalone.)
|
||||
# (The WhatsApp tray uses 📲 icon in the @whatsapp standalone.)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import rumps
|
|||
HOST = os.environ.get("MR_NUMBER_HOST", "root@45.55.191.82") # redroid box; override via env like the sh scripts
|
||||
KEY = os.path.expanduser("~/.ssh/id_ed25519_1984")
|
||||
KNOWN_HOSTS = "/tmp/redroid_known"
|
||||
CONSOLE_URL = "http://localhost:8001/ui" # combined page: 8000 screen iframe + keyboard
|
||||
CONSOLE_URL = "http://localhost:8001/ui?app=mr-number" # combined page with app-specific title for the webui
|
||||
PORTS = (8000, 8001, 8003, 5555) # 8003 = mrnumber-ocr; 5555 = adb (box becomes localhost:5555)
|
||||
ADB = os.environ.get("MR_NUMBER_ADB", "/opt/homebrew/bin/adb")
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ KEY="${MR_NUMBER_KEY:-$HOME/.ssh/id_ed25519_1984}"
|
|||
# Serial as seen from plum adb (after adb connect or the attach that mr uses).
|
||||
# Both 45.55.191.82:5555 and localhost:5555 are typically visible; use localhost
|
||||
# to match the mcp env and mr lookup.sh convention inside the box.
|
||||
SERIAL="${WHATSAPP_REDROID_SERIAL:-localhost:5555}"
|
||||
SERIAL="${WHATSAPP_REDROID_SERIAL:-localhost:5556}" # own adb port for this whatsapp console/tray (separate from mr-number 5555)
|
||||
|
||||
PHONE="${1:-}"
|
||||
if [ -z "$PHONE" ]; then
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ except ImportError:
|
|||
# --- Config / env
|
||||
QUINN_MY_URL = (os.environ.get("QUINN_MY_URL") or "https://my.transquinnftw.com").rstrip("/")
|
||||
QUINN_MY_SERVICE_TOKEN = os.environ.get("QUINN_MY_SERVICE_TOKEN", "")
|
||||
DEVICE = os.environ.get("WHATSAPP_DEVICE", "emulator-5554")
|
||||
DEVICE = os.environ.get("WHATSAPP_DEVICE", "localhost:5556") # own port for whatsapp tray/console (separate from mr-number)
|
||||
PACKAGE = "com.whatsapp"
|
||||
OUTPUT_DIR = Path(__file__).parent / "output"
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue