autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
261
skills/plane-so-farm-gateway/scripts/plane_full_combo.py
Normal file
261
skills/plane-so-farm-gateway/scripts/plane_full_combo.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
plane.so FULL COMBO - autoreg + workspace + API token gateway, end-to-end automated
|
||||
====================================================================================
|
||||
One account in ~20s, zero captcha. Pipeline per account:
|
||||
1. POST /auth/email-check/ -> confirm email free (MAGIC_CODE flow)
|
||||
2. POST /auth/magic-generate/ -> 6-digit code sent to mailbox
|
||||
3. IMAP poll -> read code from SUBJECT "Plane login code is NNNNNN"
|
||||
4. GET /auth/get-csrf-token/ -> csrftoken (mandatory, else CSRF failure page)
|
||||
5. POST /auth/magic-sign-up/ -> 302 + session-id cookie (account created, email auto-verified)
|
||||
6. GET /api/users/me/ -> verify session
|
||||
7. POST /api/workspaces/ -> create workspace (name+slug)
|
||||
8. POST /api/users/api-tokens/ -> issue API token (plane_api_*, 60/min, shown ONCE)
|
||||
9. GET /api/v1/users/me/ (X-API-Key) -> verify token live
|
||||
10. append full record to accounts.jsonl
|
||||
|
||||
Backend: api.plane.so (app.plane.so = SPA only, 405 on /auth).
|
||||
Throttle: auth 10/min per IP -> bulk sleeps 7s.
|
||||
Reverse source: github.com/makeplane/plane (preview branch):
|
||||
apps/api/plane/authentication/urls.py, views/app/magic.py, app/views/api.py
|
||||
|
||||
CONFIG: fill IMAP_* + EMAIL_* below, or pass --emails file (email:imap_host:imap_user:imap_pass).
|
||||
|
||||
Usage:
|
||||
python plane_full_combo.py <alias> # one account via +alias
|
||||
python plane_full_combo.py --bulk N [out.jsonl] # mass run
|
||||
python plane_full_combo.py --emails list.txt [out.jsonl]
|
||||
Token use afterwards:
|
||||
curl https://api.plane.so/api/v1/users/me/ -H "X-API-Key: [REDACTED]"
|
||||
workspace-scoped: https://api.plane.so/api/v1/workspaces/<slug>/projects/ etc.
|
||||
"""
|
||||
import imaplib, email as emaillib, email.utils, re, time, json, sys, random, ssl
|
||||
import urllib.request, urllib.error, http.cookiejar
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# ============ CONFIG - FILL YOUR OWN ============
|
||||
IMAP_HOST = "" # imap.gmail.com / outlook.office365.com / ...
|
||||
IMAP_USER = ""
|
||||
IMAP_PASS = "" # app password recommended
|
||||
EMAIL_DOMAIN = "" # gmail.com
|
||||
EMAIL_LOCAL = "" # yourname -> yourname+prNNNNNN@gmail.com
|
||||
WORKSPACE_NAME = "farm lab"
|
||||
WORKSPACE_SLUG_PREFIX = "farmlab"
|
||||
# ================================================
|
||||
|
||||
API = "https://api.plane.so"
|
||||
UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
"Origin": "https://app.plane.so", "Referer": "https://app.plane.so/sign-up/"}
|
||||
|
||||
|
||||
def post_json(path, payload, cookie=None, csrf=None, extra=None):
|
||||
hdrs = {"Content-Type": "application/json", **UA}
|
||||
if cookie: hdrs["Cookie"] = cookie
|
||||
if csrf: hdrs["X-CSRFToken"] = csrf
|
||||
if extra: hdrs.update(extra)
|
||||
req = Request(API + path, data=json.dumps(payload).encode(), headers=hdrs)
|
||||
try:
|
||||
with urlopen(req, timeout=30) as r:
|
||||
return r.status, json.loads(r.read().decode() or "{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
try: body = json.loads(e.read().decode() or "{}")
|
||||
except Exception: body = {}
|
||||
return e.code, body
|
||||
|
||||
|
||||
def get_csrf():
|
||||
cj = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
req = urllib.request.Request(API + "/auth/get-csrf-token/", headers=UA)
|
||||
with opener.open(req, timeout=30) as r:
|
||||
r.read()
|
||||
token = [REDACTED] for c in cj if c.name == "csrftoken"), None)
|
||||
return token, "; ".join(f"{c.name}={c.value}" for c in cj)
|
||||
|
||||
|
||||
def post_form(path, data, cookies=None, csrf=None):
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *a, **k): return None
|
||||
opener = urllib.request.build_opener(NoRedirect)
|
||||
hdrs = {"Content-Type": "application/x-www-form-urlencoded", **UA}
|
||||
if cookies: hdrs["Cookie"] = cookies
|
||||
if csrf: hdrs["X-CSRFToken"] = csrf
|
||||
req = urllib.request.Request(API + path, data=urlencode(data).encode(), headers=hdrs)
|
||||
try:
|
||||
r = opener.open(req, timeout=30)
|
||||
return r.status, r.read().decode(errors="ignore"), r.headers
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode(errors="ignore"), e.headers
|
||||
|
||||
|
||||
def get_json(path, cookie=None, api_key=None):
|
||||
hdrs = {"User-Agent": UA["User-Agent"]}
|
||||
if cookie: hdrs["Cookie"] = cookie
|
||||
if api_key: hdrs["X-API-Key"] = api_key
|
||||
req = Request(API + path, headers=hdrs)
|
||||
try:
|
||||
with urlopen(req, timeout=30) as r:
|
||||
return r.status, json.loads(r.read().decode() or "{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {}
|
||||
|
||||
|
||||
def read_otp(match_suffix, host, user, passwd, tries=24, wait=5, since_ts=None):
|
||||
for _ in range(tries):
|
||||
try:
|
||||
m = imaplib.IMAP4_SSL(host, ssl_context=ssl.create_default_context())
|
||||
m.login(user, passwd); m.select("INBOX")
|
||||
_, data = m.search(None, 'SUBJECT "Plane login code"')
|
||||
best = None
|
||||
for num in data[0].split()[-10:]:
|
||||
_, msg = m.fetch(num, "(RFC822)")
|
||||
raw = msg[0][1]
|
||||
try:
|
||||
mime = emaillib.message_from_bytes(raw)
|
||||
full = ""
|
||||
for part in mime.walk():
|
||||
if part.get_content_type() in ("text/plain", "text/html"):
|
||||
try: full += part.get_payload(decode=True).decode(errors="ignore")
|
||||
except Exception: pass
|
||||
full += " " + str(mime.get("Subject", "")) + " " + str(mime.get("To", ""))
|
||||
except Exception:
|
||||
full = raw.decode(errors="ignore")
|
||||
if match_suffix.lower() not in full.lower():
|
||||
continue
|
||||
mm = re.search(r"login code is (\d{6})", full)
|
||||
dm = re.search(r"^Date: (.+)$", raw.decode(errors="ignore"), re.M)
|
||||
ts = 0
|
||||
if dm:
|
||||
try: ts = email.utils.mktime_tz(email.utils.parsedate_tz(dm.group(1)))
|
||||
except Exception: ts = 0
|
||||
if mm and (best is None or ts >= best[0]):
|
||||
best = (ts, mm.group(1))
|
||||
m.logout()
|
||||
if best and (since_ts is None or best[0] >= since_ts - 180):
|
||||
return best[1]
|
||||
except Exception as e:
|
||||
print("imap err:", e, file=sys.stderr)
|
||||
time.sleep(wait)
|
||||
return None
|
||||
|
||||
|
||||
def full_combo(email_addr, match_suffix, imap_host, imap_user, imap_pass, slug=None):
|
||||
rec = {"email": email_addr, "ts": int(time.time())}
|
||||
# 1-2. email check + magic generate
|
||||
st, js = post_json("/auth/email-check/", {"email": email_addr})
|
||||
if js.get("existing"):
|
||||
rec["result"] = "EXISTS"; return rec
|
||||
t0 = time.time()
|
||||
st, js = post_json("/auth/magic-generate/", {"email": email_addr})
|
||||
if st != 200:
|
||||
rec["result"] = f"GEN_FAIL_{st}"; return rec
|
||||
# 3. OTP
|
||||
code = read_otp(match_suffix, imap_host, imap_user, imap_pass, since_ts=t0)
|
||||
if not code:
|
||||
rec["result"] = "NO_OTP"; return rec
|
||||
# 4-5. csrf + sign-up
|
||||
csrf_tok, csrf_cookie = get_csrf()
|
||||
st, body, hdrs = post_form("/auth/magic-sign-up/", {"email": email_addr, "code": code},
|
||||
cookies=csrf_cookie, csrf=csrf_tok)
|
||||
loc = hdrs.get("Location", "")
|
||||
if "error_code" in loc:
|
||||
rec["result"] = "SIGNUP_ERR:" + loc[:150]; return rec
|
||||
cookies = hdrs.get_all("Set-Cookie") or []
|
||||
sess = "; ".join(c.split(";")[0] for c in cookies)
|
||||
cookie = ((sess + "; ") if sess else "") + csrf_cookie
|
||||
csrf_val = next((c.split(";")[0].split("=", 1)[1] for c in cookies if c.startswith("csrftoken=")), csrf_tok)
|
||||
# 6. verify session
|
||||
st, me = get_json("/api/users/me/", cookie=cookie)
|
||||
if st != 200 or not me.get("email"):
|
||||
rec["result"] = f"SESSION_FAIL_{st}"; return rec
|
||||
rec["user_id"] = me["id"]
|
||||
# 7. create workspace
|
||||
slug = slug or (WORKSPACE_SLUG_PREFIX + str(random.randint(10000, 99999)))
|
||||
st, ws = post_json("/api/workspaces/", {"name": WORKSPACE_NAME, "slug": slug},
|
||||
cookie=cookie, csrf=csrf_val,
|
||||
extra={"Referer": "https://app.plane.so/"})
|
||||
if st not in (200, 201):
|
||||
rec["result"] = f"WS_FAIL_{st}:{str(ws)[:100]}"; rec["session_cookie"] = cookie; return rec
|
||||
rec["workspace"] = {"slug": ws.get("slug"), "id": ws.get("id")}
|
||||
# 8. API token
|
||||
st, tk = post_json("/api/users/api-tokens/", {"label": "combo-" + slug, "description": "farm"},
|
||||
cookie=cookie, csrf=csrf_val,
|
||||
extra={"Referer": "https://app.plane.so/"})
|
||||
if st not in (200, 201) or not tk.get("token"):
|
||||
rec["result"] = f"TOKEN_FAIL_{st}:{str(tk)[:100]}"; rec["session_cookie"] = cookie; return rec
|
||||
rec["api_token"] = tk["token"]
|
||||
rec["rate_limit"] = tk.get("allowed_rate_limit")
|
||||
# 9. verify token on public v1
|
||||
st, v1 = get_json("/api/v1/users/me/", api_key=tk["token"])
|
||||
rec["token_live"] = (st == 200)
|
||||
rec["session_cookie"] = cookie
|
||||
rec["result"] = "FULL_OK" if st == 200 else "TOKEN_UNVERIFIED"
|
||||
return rec
|
||||
|
||||
|
||||
def gen_alias_email():
|
||||
alias = "pr" + str(random.randint(100000, 999999))
|
||||
return f"{EMAIL_LOCAL}+{alias}@{EMAIL_DOMAIN}", alias
|
||||
|
||||
|
||||
def run_one(target, imap_host=None, imap_user=None, imap_pass=None):
|
||||
h = imap_host or IMAP_HOST; u = imap_user or IMAP_USER; p = imap_pass or IMAP_PASS
|
||||
if "@" in target:
|
||||
addr, suffix = target, target
|
||||
else:
|
||||
if not (EMAIL_LOCAL and EMAIL_DOMAIN):
|
||||
print("ERROR: fill EMAIL_LOCAL/EMAIL_DOMAIN or pass full email"); sys.exit(1)
|
||||
addr = f"{EMAIL_LOCAL}+{target}@{EMAIL_DOMAIN}"; suffix = target
|
||||
rec = full_combo(addr, suffix, h, u, p)
|
||||
print(json.dumps(rec, indent=2, default=str))
|
||||
return rec
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print(__doc__); sys.exit(1)
|
||||
if args[0] == "--bulk":
|
||||
n = int(args[1]) if len(args) > 1 else 10
|
||||
outfile = args[2] if len(args) > 2 else "plane_accounts.jsonl"
|
||||
ok = 0
|
||||
with open(outfile, "a", encoding="utf-8") as f:
|
||||
for i in range(n):
|
||||
addr, alias = gen_alias_email()
|
||||
try:
|
||||
rec = full_combo(addr, alias, IMAP_HOST, IMAP_USER, IMAP_PASS)
|
||||
except Exception as e:
|
||||
rec = {"email": addr, "result": "EXC:" + str(e)}
|
||||
f.write(json.dumps(rec, default=str) + "\n"); f.flush()
|
||||
if rec.get("result") == "FULL_OK": ok += 1
|
||||
print(f"[{i+1}/{n}] {addr} -> {rec.get('result')} token={rec.get('api_token','-')[:20]} (full_ok={ok})")
|
||||
time.sleep(7)
|
||||
print(f"DONE {ok}/{n} -> {outfile}")
|
||||
elif args[0] == "--emails":
|
||||
outfile = args[2] if len(args) > 2 else "plane_accounts.jsonl"
|
||||
ok = 0
|
||||
with open(args[1], encoding="utf-8") as f, open(outfile, "a", encoding="utf-8") as o:
|
||||
for i, line in enumerate(f):
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
parts = line.split(":")
|
||||
if len(parts) == 4:
|
||||
addr, h, u, p = parts
|
||||
else:
|
||||
addr, h, u, p = line, IMAP_HOST, IMAP_USER, IMAP_PASS
|
||||
try:
|
||||
rec = full_combo(addr, addr, h, u, p)
|
||||
except Exception as e:
|
||||
rec = {"email": addr, "result": "EXC:" + str(e)}
|
||||
o.write(json.dumps(rec, default=str) + "\n"); o.flush()
|
||||
if rec.get("result") == "FULL_OK": ok += 1
|
||||
print(f"[{i+1}] {addr} -> {rec.get('result')} (full_ok={ok})")
|
||||
time.sleep(7)
|
||||
print(f"DONE {ok} -> {outfile}")
|
||||
else:
|
||||
run_one(args[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user