6d224861 came from a --no-checkout worktree whose empty index made the
commit drop every file except two. This restores 05b50d2b's full tree and
correctly adds stacks/stem95su/gdrive-sync.tf + the service-catalog stem95su
entry. Forward-only (parent=6d224861, no force-push); [ci skip] since the
live infra was never applied from the broken commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
5.1 KiB
Python
163 lines
5.1 KiB
Python
"""
|
|
MAM bonus-point spender — tier-aware, pay-what-we-owe.
|
|
|
|
MAM's bonusBuy.php API enforces a hard 50 GiB minimum per purchase
|
|
("Automated spenders are limited to buying at least 50 GB... due to log
|
|
spam"). Valid API tiers are 50, 100, 200, 500 GiB (@ 500 BP/GiB). That
|
|
means the "pay exactly what we owe" approach from the recovery plan
|
|
rounds UP to 50 GiB for the first purchase — small buys can only be done
|
|
via the web UI, not the API.
|
|
|
|
Logic: pick the smallest valid tier that both (a) satisfies the ratio
|
|
deficit and (b) we can afford without burning the BP reserve. Skip if
|
|
nothing fits; the cron will retry in 6 h once BP grows.
|
|
"""
|
|
import math
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
import requests
|
|
|
|
PUSHGW = "http://prometheus-prometheus-pushgateway.monitoring:9091"
|
|
COOKIE_FILE = "/data/mam_id"
|
|
|
|
TARGET_RATIO = float(os.environ.get("TARGET_RATIO", "2.0"))
|
|
RESERVE_BP = int(os.environ.get("RESERVE_BP", "500"))
|
|
BP_PER_GB = int(os.environ.get("BP_PER_GB", "500"))
|
|
# MAM-enforced minimum purchase for API callers: 50 GiB.
|
|
API_TIERS_GIB = (50, 100, 200, 500)
|
|
|
|
CLASS_CODES = {
|
|
"Mouse": 0,
|
|
"Vole": 1,
|
|
"User": 2,
|
|
"Power User": 3,
|
|
"Elite": 4,
|
|
"Torrent Master": 5,
|
|
"Power TM": 6,
|
|
"Elite TM": 7,
|
|
"VIP": 8,
|
|
}
|
|
|
|
|
|
def save_cookie(resp):
|
|
for c in resp.cookies:
|
|
if c.name == "mam_id":
|
|
fd, tmp = tempfile.mkstemp(dir="/data")
|
|
os.write(fd, c.value.encode())
|
|
os.close(fd)
|
|
os.rename(tmp, COOKIE_FILE)
|
|
return
|
|
|
|
|
|
def push(metrics):
|
|
try:
|
|
requests.post(
|
|
f"{PUSHGW}/metrics/job/mam-bp-spender", data=metrics, timeout=10
|
|
)
|
|
except Exception as e:
|
|
print(f"pushgateway error: {e}", file=sys.stderr)
|
|
|
|
|
|
def load_cookie():
|
|
if os.path.exists(COOKIE_FILE):
|
|
return open(COOKIE_FILE).read().strip()
|
|
return os.environ.get("MAM_ID", "")
|
|
|
|
|
|
def main():
|
|
mam_id = load_cookie()
|
|
if not mam_id:
|
|
print("No mam_id available", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
s = requests.Session()
|
|
s.cookies.set("mam_id", mam_id, domain=".myanonamouse.net")
|
|
|
|
r = s.get("https://www.myanonamouse.net/jsonLoad.php", timeout=15)
|
|
if r.status_code != 200:
|
|
push("mam_farming_cookie_expired 1\n")
|
|
print(f"Cookie expired: {r.status_code}", file=sys.stderr)
|
|
sys.exit(1)
|
|
save_cookie(r)
|
|
|
|
profile = r.json()
|
|
ratio = float(profile.get("ratio", 0) or 0)
|
|
classname = profile.get("classname", "Mouse")
|
|
class_code = CLASS_CODES.get(classname, 0)
|
|
# MAM returns `downloaded`/`uploaded` as pretty strings ("715.55 MiB");
|
|
# `*_bytes` are the authoritative integer fields.
|
|
downloaded = int(profile.get("downloaded_bytes", 0) or 0)
|
|
uploaded = int(profile.get("uploaded_bytes", 0) or 0)
|
|
bp = int(float(profile.get("seedbonus", 0) or 0))
|
|
|
|
deficit_bytes = max(0, int(downloaded * TARGET_RATIO) - uploaded)
|
|
needed_gib = math.ceil(deficit_bytes / (1024**3)) + 1 if deficit_bytes > 0 else 0
|
|
affordable_gib = max(0, (bp - RESERVE_BP) // BP_PER_GB)
|
|
|
|
# Pick the smallest API tier that satisfies the deficit AND fits the
|
|
# budget. If even the smallest tier is too expensive, skip — the cron
|
|
# will retry in 6 h once BP has grown.
|
|
buy_gib = 0
|
|
for tier in API_TIERS_GIB:
|
|
if tier >= needed_gib and tier <= affordable_gib:
|
|
buy_gib = tier
|
|
break
|
|
if buy_gib == 0 and needed_gib > 0 and affordable_gib >= API_TIERS_GIB[0]:
|
|
# Deficit exceeds all tiers we can afford — buy the largest
|
|
# tier that fits to make progress.
|
|
for tier in reversed(API_TIERS_GIB):
|
|
if tier <= affordable_gib:
|
|
buy_gib = tier
|
|
break
|
|
|
|
print(
|
|
f"Profile: ratio={ratio} class={classname} "
|
|
f"DL={downloaded / 1024**3:.2f} GiB UL={uploaded / 1024**3:.2f} GiB "
|
|
f"BP={bp} | deficit={deficit_bytes / 1024**3:.2f} GiB "
|
|
f"needed={needed_gib} affordable={affordable_gib} buy={buy_gib}"
|
|
)
|
|
|
|
spent_gib = 0
|
|
if buy_gib >= API_TIERS_GIB[0]:
|
|
time.sleep(3)
|
|
url = (
|
|
"https://www.myanonamouse.net/json/bonusBuy.php"
|
|
f"?spendtype=upload&amount={buy_gib}"
|
|
)
|
|
r2 = s.get(url, timeout=15)
|
|
save_cookie(r2)
|
|
try:
|
|
body = r2.json()
|
|
except ValueError:
|
|
body = {}
|
|
ok = r2.status_code == 200 and body.get("success") is True
|
|
print(
|
|
f"Buy {buy_gib} GiB -> {r2.status_code} "
|
|
f"success={body.get('success')} {r2.text[:160]}"
|
|
)
|
|
if ok:
|
|
spent_gib = buy_gib
|
|
|
|
metrics = (
|
|
"mam_farming_cookie_expired 0\n"
|
|
f"mam_ratio {ratio}\n"
|
|
f'mam_class_code{{classname="{classname}"}} {class_code}\n'
|
|
f"mam_downloaded_bytes {downloaded}\n"
|
|
f"mam_uploaded_bytes {uploaded}\n"
|
|
f"mam_bp_balance {bp}\n"
|
|
f"mam_bp_spent_gb {spent_gib}\n"
|
|
f"mam_bp_needed_gib {needed_gib}\n"
|
|
f"mam_bp_affordable_gib {affordable_gib}\n"
|
|
)
|
|
push(metrics)
|
|
print(
|
|
f"Done: BP={bp}, spent={spent_gib} GiB (needed={needed_gib}, "
|
|
f"affordable={affordable_gib})"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|