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:
Natalie 2026-06-28 13:58:23 -04:00
parent bd7b5e8f47
commit 6fdcf8df7c
5 changed files with 60 additions and 29 deletions

View file

@ -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 &rarr; redroid</h3>
<div class="header">{label} adb keyboard passthrough</div>
<h3>Keyboard &rarr; redroid</h3>
<p style=color:#888>Click a field in the console first. Then click the box below and just type &mdash; 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 &amp; 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")

View file

@ -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")"

View file

@ -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")

View file

@ -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

View file

@ -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)