chore(tools-generic): 🔧 Update generic tooling scripts and configurations

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-04 11:09:03 -07:00
parent 9d58e7e6e3
commit a0c9def323
2 changed files with 373 additions and 0 deletions

373
tools/rank-check.py Executable file
View file

@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""
rank-check.py SEO rank checker for transquinnftw.com destination pages.
For each target city:
1. Brings up the city's WireGuard config via wg-quick
2. Searches DuckDuckGo for "trans escort [city]"
3. Records position of transquinnftw.com in results (None = not in top 30)
4. Appends results to tools/rank-results.json
Setup:
1. Download ProtonVPN WireGuard configs from account.protonvpn.com
Downloads WireGuard configuration select server download .conf
2. Drop each .conf in tools/vpn-configs/ named by city slug:
tools/vpn-configs/paris.conf
tools/vpn-configs/london.conf
... etc.
3. Run: sudo python3 tools/rank-check.py
(wg-quick requires root)
Usage:
sudo python3 tools/rank-check.py # all cities
sudo python3 tools/rank-check.py --city paris # single city
sudo python3 tools/rank-check.py --no-vpn # skip VPN (no sudo needed)
python3 tools/rank-check.py --dry-run # print plan only
"""
import argparse
import json
import os
import random
import subprocess
import sys
import time
from datetime import date, datetime
from pathlib import Path
from typing import Optional
from urllib.parse import unquote, urlparse, parse_qs
import requests
from bs4 import BeautifulSoup
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
TARGET_DOMAIN = "transquinnftw.com"
TOOLS_DIR = Path(__file__).parent
RESULTS_FILE = TOOLS_DIR / "rank-results.json"
VPN_CONFIGS_DIR = TOOLS_DIR / "vpn-configs"
DDG_URL = "https://html.duckduckgo.com/html/"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://duckduckgo.com/",
}
# slug → (display name, search query)
DESTINATIONS: dict[str, tuple[str, str]] = {
"paris": ("Paris", "trans escort Paris"),
"london": ("London", "trans escort London"),
"venice": ("Venice", "trans escort Venice"),
"reykjavik": ("Reykjavík", "trans escort Reykjavik"),
"new-york-city": ("New York City", "trans escort New York"),
"las-vegas": ("Las Vegas", "trans escort Las Vegas"),
"los-angeles": ("Los Angeles", "trans escort Los Angeles"),
"chicago": ("Chicago", "trans escort Chicago"),
"tokyo": ("Tokyo", "trans escort Tokyo"),
"kyoto": ("Kyoto", "trans escort Kyoto"),
"seoul": ("Seoul", "trans escort Seoul"),
"ibiza": ("Ibiza", "trans escort Ibiza"),
"honolulu": ("Honolulu", "trans escort Honolulu"),
"seattle": ("Seattle", "trans escort Seattle"),
"vancouver": ("Vancouver", "trans escort Vancouver"),
"anchorage": ("Anchorage", "trans escort Anchorage"),
"glasgow": ("Glasgow", "trans escort Glasgow"),
"dublin": ("Dublin", "trans escort Dublin"),
}
# ---------------------------------------------------------------------------
# WireGuard helpers
# ---------------------------------------------------------------------------
def conf_path(slug: str) -> Path:
return VPN_CONFIGS_DIR / f"{slug}.conf"
def vpn_up(slug: str) -> tuple[bool, str]:
"""Bring up WireGuard interface for slug. Returns (ok, exit_ip)."""
conf = conf_path(slug)
if not conf.exists():
return False, ""
# Derive interface name from conf filename (wg-quick requirement: ≤15 chars, alphanum+_)
iface = slug.replace("-", "")[:15]
target_conf = Path(f"/tmp/wg-{iface}.conf")
# Write a copy with a safe interface name
target_conf.write_text(conf.read_text())
try:
result = subprocess.run(
["wg-quick", "up", str(target_conf)],
capture_output=True, text=True, timeout=20,
)
if result.returncode != 0:
print(f" ✗ wg-quick up failed: {result.stderr.strip()}")
target_conf.unlink(missing_ok=True)
return False, ""
except subprocess.TimeoutExpired:
print(" ✗ wg-quick timed out")
target_conf.unlink(missing_ok=True)
return False, ""
time.sleep(2) # let routing settle
exit_ip = current_ip()
return True, exit_ip
def vpn_down(slug: str) -> None:
iface = slug.replace("-", "")[:15]
target_conf = Path(f"/tmp/wg-{iface}.conf")
if target_conf.exists():
subprocess.run(
["wg-quick", "down", str(target_conf)],
capture_output=True, timeout=15,
)
target_conf.unlink(missing_ok=True)
def current_ip() -> str:
try:
return requests.get("https://api.ipify.org", timeout=8, headers={"User-Agent": "curl/8.0"}).text.strip()
except Exception:
return "unknown"
def missing_configs(slugs: list[str]) -> list[str]:
return [s for s in slugs if not conf_path(s).exists()]
# ---------------------------------------------------------------------------
# Rank checking
# ---------------------------------------------------------------------------
def search_ddg(query: str) -> list[str]:
"""Search DuckDuckGo, return ordered result URLs (up to ~30 across 3 pages)."""
session = requests.Session()
session.headers.update(HEADERS)
urls: list[str] = []
resp = None
try:
resp = session.post(DDG_URL, data={"q": query, "b": ""}, timeout=20)
resp.raise_for_status()
urls.extend(_parse_ddg(resp.text))
except Exception as e:
print(f" ✗ DDG request failed: {e}")
return urls
for _ in range(2):
if any(TARGET_DOMAIN in u for u in urls):
break
time.sleep(random.uniform(4, 8))
try:
soup = BeautifulSoup(resp.text, "html.parser")
next_form = soup.find("form", {"class": "nav-link"})
if not next_form:
break
params = {
inp["name"]: inp.get("value", "")
for inp in next_form.find_all("input")
if inp.get("name")
}
resp = session.post(DDG_URL, data=params, timeout=20)
resp.raise_for_status()
urls.extend(_parse_ddg(resp.text))
except Exception:
break
return urls
def _parse_ddg(html: str) -> list[str]:
soup = BeautifulSoup(html, "html.parser")
results: list[str] = []
for a in soup.select("a.result__url"):
href = a.get("href", "")
if href and "duckduckgo.com" not in href:
results.append(href)
for a in soup.select("a.result__a"):
href = a.get("href", "")
if "uddg=" in href:
qs = parse_qs(urlparse(href).query)
uddg = qs.get("uddg", [])
if uddg:
results.append(unquote(uddg[0]))
# Deduplicate preserving order
seen: set[str] = set()
deduped: list[str] = []
for u in results:
if u not in seen:
seen.add(u)
deduped.append(u)
return deduped
def find_rank(urls: list[str]) -> Optional[int]:
for i, url in enumerate(urls, 1):
if TARGET_DOMAIN in url:
return i
return None
def check_indexed(slug: str) -> Optional[bool]:
query = f"site:{TARGET_DOMAIN}/destinations/{slug}"
time.sleep(random.uniform(3, 6))
urls = search_ddg(query)
if not urls:
return None
return any(TARGET_DOMAIN in u for u in urls)
# ---------------------------------------------------------------------------
# Results persistence
# ---------------------------------------------------------------------------
def load_results() -> list[dict]:
if RESULTS_FILE.exists():
return json.loads(RESULTS_FILE.read_text())
return []
def save_results(results: list[dict]) -> None:
RESULTS_FILE.write_text(json.dumps(results, indent=2, ensure_ascii=False))
def print_summary(run: dict) -> None:
print(f"\n{'' * 64}")
print(f" {run['date']} | VPN: {'on' if run['vpn'] else 'off'} | {run['duration_s']}s total")
print(f"{'' * 64}")
for r in run["cities"]:
indexed_sym = "" if r["indexed"] else ("" if r["indexed"] is False else "?")
rank_str = f"#{r['rank']}" if r["rank"] else "not in top 30"
ip_str = f" [{r['exit_ip']}]" if r.get("exit_ip") else ""
print(f" {indexed_sym} {r['city']:<20} {rank_str:<16}{ip_str}")
print()
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Rank checker — transquinnftw.com")
parser.add_argument("--no-vpn", action="store_true", help="Skip VPN rotation")
parser.add_argument("--city", metavar="SLUG", help="Check a single city by slug")
parser.add_argument("--dry-run", action="store_true", help="Print plan, no requests")
args = parser.parse_args()
if args.city and args.city not in DESTINATIONS:
print(f"Unknown slug '{args.city}'. Valid slugs:\n {chr(10).join(DESTINATIONS)}")
sys.exit(1)
targets: dict[str, tuple[str, str]] = (
{args.city: DESTINATIONS[args.city]} if args.city else DESTINATIONS
)
if args.dry_run:
print("Dry run\n")
missing = missing_configs(list(targets)) if not args.no_vpn else []
for slug, (city, query) in targets.items():
conf_status = ""
if not args.no_vpn:
conf_status = " ✓ conf" if conf_path(slug).exists() else " ✗ missing conf"
print(f" {slug:<20} \"{query}\"{conf_status}")
if missing:
print(f"\n Missing configs for: {', '.join(missing)}")
print(f" Download from: account.protonvpn.com → Downloads → WireGuard")
print(f" Place in: {VPN_CONFIGS_DIR}/")
return
if not args.no_vpn and os.geteuid() != 0:
print("wg-quick requires root. Run with sudo, or use --no-vpn.")
sys.exit(1)
missing = missing_configs(list(targets)) if not args.no_vpn else []
if missing and not args.no_vpn:
print(f"Missing WireGuard configs for: {', '.join(missing)}")
print(f"Download from account.protonvpn.com → Downloads → WireGuard")
print(f"Place .conf files in: {VPN_CONFIGS_DIR}/")
print(f"\nCities with configs will still run. Continue? [y/N] ", end="")
if input().strip().lower() != "y":
sys.exit(0)
start = time.time()
run: dict = {
"date": date.today().isoformat(),
"timestamp": datetime.utcnow().isoformat() + "Z",
"vpn": not args.no_vpn,
"cities": [],
"duration_s": 0,
}
slugs = list(targets)
for i, slug in enumerate(slugs):
city, query = targets[slug]
print(f"\n[{city}]")
exit_ip: Optional[str] = None
vpn_active = False
if not args.no_vpn:
if conf_path(slug).exists():
print(f" → VPN up ({slug}.conf)...", end=" ", flush=True)
ok, exit_ip = vpn_up(slug)
if ok:
vpn_active = True
print(f"connected {exit_ip}")
else:
print("failed — running without VPN for this city")
else:
print(f" ! No conf for {slug} — skipping VPN")
print(f"\"{query}\"")
urls = search_ddg(query)
rank = find_rank(urls)
rank_str = f"#{rank}" if rank else "not in top 30"
print(f" → rank: {rank_str} ({len(urls)} results parsed)")
print(f" → index check...", end=" ", flush=True)
indexed = check_indexed(slug)
print("indexed ✓" if indexed else "not indexed ✗" if indexed is False else "unknown")
run["cities"].append({
"slug": slug,
"city": city,
"query": query,
"rank": rank,
"indexed": indexed,
"exit_ip": exit_ip,
"result_count": len(urls),
})
if vpn_active:
vpn_down(slug)
if i < len(slugs) - 1:
delay = random.uniform(8, 15)
print(f" → pause {delay:.0f}s")
time.sleep(delay)
run["duration_s"] = round(time.time() - start)
existing = load_results()
existing.append(run)
save_results(existing)
print_summary(run)
print(f"Saved → {RESULTS_FILE}")
if __name__ == "__main__":
main()

View file