Skill: ganidhuz/ganidhuz-foxx (version 1.0.0)

--- name: ganidhuz-foxx description: > 🦊 Ganidhuz-FoxX (Firefox + X combined lol). Browse X/Twitter using a real logged-in Firefox session via cookie injection. Supports profile viewing, tweet fetching, searching, and scrolling feeds — no API key, no bot blocks. license: MIT --- # Ganidhuz-FoxX 🦊 Browse X/Twitter through Firefox with your real session cookies. Built because Chromium kept getting bot-blocked by X. ## Requirements - Python 3.7+ - Playwright: `pip install playwright && playwright install firefox` - Firefox installed with an active X/Twitter login - Xvfb display (for headless servers): `Xvfb :1 &` ## Setup ### 1. Export your X cookies Close Firefox first, then: ```bash bash scripts/export-x-cookies.sh # Cookies saved to secrets/x-cookies.json by default # Override: FOXX_COOKIES_OUT=/custom/path.json bash scripts/export-x-cookies.sh ``` Custom Firefox profile path: ```bash FIREFOX_PROFILE_PATH=/path/to/profile bash scripts/export-x-cookies.sh ``` ### 2. Health check ```bash bash scripts/check-firefox-env.sh ``` ## Usage Run a plan file: ```bash DISPLAY=:1 python3 scripts/playwright-firefox-control.py --plan /tmp/foxx-plan.json ``` ## Plan Examples ### View a profile ```json { "needs_gui": true, "gui_reason": "site_only_action", "url": "https://x.com/elonmusk", "cookies_path": "secrets/x-cookies.json", "steps": [ {"action": "wait", "ms": 4000}, {"action": "screenshot", "path": "/tmp/foxx-profile.png"} ], "close_delay_ms": 3000 } ``` ### Search tweets (live) ```json { "needs_gui": true, "gui_reason": "site_only_action", "url": "https://x.com/search?q=AI+agents&src=typed_query&f=live", "cookies_path": "secrets/x-cookies.json", "steps": [ {"action": "wait", "ms": 4000}, {"action": "screenshot", "path": "/tmp/foxx-search.png"} ], "close_delay_ms": 3000 } ``` ### Fetch a tweet ```json { "needs_gui": true, "gui_reason": "site_only_action", "url": "https://x.com/user/status/123456789", "cookies_path": "secrets/x-cookies.json", "steps": [ {"action": "wait", "ms": 3000}, {"action": "content", "selector": "article"}, {"action": "screenshot", "path": "/tmp/foxx-tweet.png"} ], "close_delay_ms": 3000 } ``` ## Plan Options | Field | Default | Description | |---|---|---| | `needs_gui` | required | Must be `true` to launch browser | | `gui_reason` | required | One of: `login`, `captcha`, `mfa`, `visual_verification`, `site_only_action` | | `url` | required | Starting URL | | `cookies_path` | optional | Path to exported cookies JSON | | `close_delay_ms` | `3000` | Wait (ms) before closing browser - validate result first | | `validation_screenshot` | `/tmp/firefox-openclaw-validate.png` | Auto-taken final screenshot before close | | `storage_state_path` | optional | Save session state to this path after run | ## Supported Step Actions - `goto` - navigate to URL - `click` - click element by selector - `fill` - fill input by selector - `type` - type text with delay - `press` - press keyboard key - `wait` - wait ms - `wait_for_selector` - wait for element - `screenshot` - take screenshot - `content` - extract inner text from element ## Behaviour Rules - Always wait for page load (`wait` step, min 3000ms recommended) - A validation screenshot is always taken before closing - Browser waits `close_delay_ms` before closing - verify result is correct - If cookies expired (redirects to login), re-run `export-x-cookies.sh` - Always close browser after task - don't leave it idle

path: scripts/check-firefox-env.sh
content: #!/bin/bash
# Ganidhuz-FoxX: Check Firefox environment

echo "Checking Firefox environment..."

# Firefox binary
if command -v firefox &>/dev/null; then
    echo "OK: firefox binary found at $(command -v firefox)"
else
    echo "WARN: firefox not found in PATH"
fi

# X display
DISPLAY_CHECK="${DISPLAY:-:1}"
if xdpyinfo -display "$DISPLAY_CHECK" &>/dev/null 2>&1; then
    echo "OK: X display $DISPLAY_CHECK reachable"
else
    echo "WARN: X display $DISPLAY_CHECK not reachable"
fi

# Playwright
if python3 -c "import playwright" &>/dev/null 2>&1; then
    echo "OK: playwright Python package found"
else
    echo "WARN: playwright not installed. Run: pip install playwright && playwright install firefox"
fi

echo "Done."


path: scripts/export-x-cookies.sh
content: #!/bin/bash
# Ganidhuz-FoxX: Export X/Twitter cookies from real Firefox snap profile
# Run this when session expires. Close Firefox first!
# 
# Config via env vars:
#   FIREFOX_PROFILE_PATH  - path to Firefox profile dir (default: auto-detect)
#   FOXX_COOKIES_OUT      - output path for cookies JSON (default: ./secrets/x-cookies.json)

set -e

# Auto-detect Firefox profile
if [ -n "$FIREFOX_PROFILE_PATH" ]; then
    PROFILE_PATH="$FIREFOX_PROFILE_PATH"
elif [ -f "$HOME/snap/firefox/common/.mozilla/firefox/profiles.ini" ]; then
    PROFILE_DIR="$HOME/snap/firefox/common/.mozilla/firefox"
    PROFILE_NAME=$(grep "^Path=" "$PROFILE_DIR/profiles.ini" | head -1 | cut -d= -f2)
    PROFILE_PATH="$PROFILE_DIR/$PROFILE_NAME"
elif [ -f "$HOME/.mozilla/firefox/profiles.ini" ]; then
    PROFILE_DIR="$HOME/.mozilla/firefox"
    PROFILE_NAME=$(grep "^Path=" "$PROFILE_DIR/profiles.ini" | head -1 | cut -d= -f2)
    PROFILE_PATH="$PROFILE_DIR/$PROFILE_NAME"
else
    echo "❌ Could not find Firefox profile. Set FIREFOX_PROFILE_PATH env var."
    exit 1
fi

DB="$PROFILE_PATH/cookies.sqlite"
TMP_DB="/tmp/foxx-cookies-copy.sqlite"
OUT="${FOXX_COOKIES_OUT:-$(dirname "$0")/../secrets/x-cookies.json}"

mkdir -p "$(dirname "$OUT")"

echo "🦊 Ganidhuz-FoxX: Exporting X cookies from $PROFILE_PATH"

pkill -f firefox 2>/dev/null && sleep 2 || true

cp "$DB" "$TMP_DB"

python3 - << EOF
import sqlite3, json

conn = sqlite3.connect("$TMP_DB")
rows = conn.execute("""
    SELECT host, name, value, path, expiry, isSecure, isHttpOnly, sameSite
    FROM moz_cookies
    WHERE host LIKE '%twitter%' OR host LIKE '%x.com%'
""").fetchall()

cookies = []
for r in rows:
    exp = r[4]
    if exp > 1e10:
        exp = int(exp / 1000)
    elif exp < -1:
        exp = -1
    cookies.append({
        "domain": r[0], "name": r[1], "value": r[2],
        "path": r[3], "expires": exp, "secure": bool(r[5]),
        "httpOnly": bool(r[6]),
        "sameSite": ["None","Lax","Strict"][r[7]] if r[7] < 3 else "None"
    })
conn.close()

with open("$OUT", "w") as f:
    json.dump({"cookies": cookies}, f, indent=2)

print(f"✅ Exported {len(cookies)} cookies -> $OUT")
EOF


path: scripts/playwright-firefox-control.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
from pathlib import Path

from playwright.sync_api import sync_playwright


def load_plan(plan_path: str) -> dict:
    if plan_path == "-":
        return json.load(sys.stdin)
    with open(plan_path, "r", encoding="utf-8") as f:
        return json.load(f)


def run_step(page, step: dict) -> dict:
    action = step.get("action")
    if action == "goto":
        url = step["url"]
        wait_until = step.get("wait_until", "domcontentloaded")
        page.goto(url, wait_until=wait_until)
        return {"action": action, "ok": True, "url": page.url}

    if action == "click":
        page.locator(step["selector"]).first.click(timeout=step.get("timeout_ms", 10000))
        return {"action": action, "ok": True}

    if action == "fill":
        page.locator(step["selector"]).first.fill(step.get("text", ""), timeout=step.get("timeout_ms", 10000))
        return {"action": action, "ok": True}

    if action == "type":
        page.locator(step["selector"]).first.type(
            step.get("text", ""),
            delay=step.get("delay_ms", 50),
            timeout=step.get("timeout_ms", 10000),
        )
        return {"action": action, "ok": True}

    if action == "press":
        page.keyboard.press(step["key"])
        return {"action": action, "ok": True}

    if action == "wait":
        page.wait_for_timeout(step.get("ms", 1000))
        return {"action": action, "ok": True}

    if action == "wait_for_selector":
        page.locator(step["selector"]).first.wait_for(timeout=step.get("timeout_ms", 10000))
        return {"action": action, "ok": True}

    if action == "screenshot":
        path = step.get("path", "/tmp/firefox-openclaw-step.png")
        Path(path).parent.mkdir(parents=True, exist_ok=True)
        page.screenshot(path=path, full_page=step.get("full_page", False))
        return {"action": action, "ok": True, "path": path}

    if action == "content":
        text = page.locator(step["selector"]).first.inner_text(timeout=step.get("timeout_ms", 10000))
        return {"action": action, "ok": True, "text": text}

    raise ValueError(f"Unsupported action: {action}")


def main() -> int:
    parser = argparse.ArgumentParser(description="Run structured Firefox automation steps with Playwright.")
    parser.add_argument("--plan", required=True, help="Path to JSON plan file, or '-' for stdin")
    parser.add_argument("--display", default=os.environ.get("DISPLAY", ":1"))
    parser.add_argument("--profile-dir", default="/root/snap/firefox/common/.mozilla/firefox/kp5hskbz.default")
    parser.add_argument("--headless", action="store_true")
    parser.add_argument("--output", default="-", help="Where to write JSON results, '-' for stdout")
    args = parser.parse_args()

    plan = load_plan(args.plan)
    os.environ["DISPLAY"] = args.display

    needs_gui = bool(plan.get("needs_gui", False))
    gui_reason = str(plan.get("gui_reason", "")).strip()
    allowed_reasons = {
        "login",
        "captcha",
        "mfa",
        "visual_verification",
        "site_only_action",
    }

    if not needs_gui:
        result = {
            "ok": True,
            "skipped": True,
            "reason": "GUI launch blocked by policy: set needs_gui=true in plan.",
        }
        payload = json.dumps(result, ensure_ascii=True, indent=2)
        if args.output == "-":
            print(payload)
        else:
            Path(args.output).parent.mkdir(parents=True, exist_ok=True)
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(payload + "\n")
        return 0

    if gui_reason not in allowed_reasons:
        result = {
            "ok": False,
            "skipped": True,
            "reason": "Invalid or missing gui_reason.",
            "allowed_gui_reasons": sorted(allowed_reasons),
        }
        payload = json.dumps(result, ensure_ascii=True, indent=2)
        if args.output == "-":
            print(payload)
        else:
            Path(args.output).parent.mkdir(parents=True, exist_ok=True)
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(payload + "\n")
        return 3

    profile_dir = plan.get("profile_dir", args.profile_dir)
    headless = bool(plan.get("headless", args.headless))
    initial_url = plan.get("url", "about:blank")
    steps = plan.get("steps", [])

    Path(profile_dir).mkdir(parents=True, exist_ok=True)

    results = {"ok": True, "display": args.display, "profile_dir": profile_dir, "steps": []}

    with sync_playwright() as p:
        browser = p.firefox.launch(headless=headless)
        context = browser.new_context()

        # Inject cookies from file if provided in plan
        cookies_path = plan.get("cookies_path")
        if cookies_path and os.path.exists(cookies_path):
            import json as _json
            with open(cookies_path) as cf:
                cookie_data = _json.load(cf)
            context.add_cookies(cookie_data.get("cookies", []))

        page = context.new_page()
        page.goto(initial_url)
        for step in steps:
            try:
                step_result = run_step(page, step)
                results["steps"].append(step_result)
            except Exception as exc:
                results["ok"] = False
                results["steps"].append(
                    {"action": step.get("action"), "ok": False, "error": str(exc), "url": page.url}
                )
                break

        results["final_url"] = page.url

        # Validate: take a final confirmation screenshot before closing
        validation_path = plan.get("validation_screenshot", "/tmp/firefox-openclaw-validate.png")
        page.screenshot(path=validation_path)
        results["validation_screenshot"] = validation_path

        # Wait before closing - give time to confirm result is correct
        close_delay_ms = plan.get("close_delay_ms", 3000)
        page.wait_for_timeout(close_delay_ms)

        if plan.get("storage_state_path"):
            context.storage_state(path=plan["storage_state_path"])
        context.close()
        browser.close()

    payload = json.dumps(results, ensure_ascii=True, indent=2)
    if args.output == "-":
        print(payload)
    else:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(payload + "\n")

    return 0 if results["ok"] else 2


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)