12 KiB
Misskey/Sharkey (Universe Federation) Registration Pattern
Instance: dc.hhhl.cc
Software: Sharkey fork "Universe Federation" v2025.5.2-dev
Captcha: Cloudflare Turnstile (sitekey: 0x4AAAAAADUhQ0UI_7UTXChP)
Email restriction: Only gmail.com and qq.com accepted
Email verification: Required for signup. Verification emails land in Gmail SPAM.
API Endpoints
# Instance meta (POST required)
POST https://dc.hhhl.cc/api/meta
Body: {}
# Signup (requires Turnstile token)
POST https://dc.hhhl.cc/api/signup
Body: {"username":"...","password":"...","emailAddress":"...","turnstileResponse":"..."}
# List users (public)
POST https://dc.hhhl.cc/api/users
Body: {}
# Check user exists (ActivityPub)
GET https://dc.hhhl.cc/@username
Accept: application/activity+json
# 404 = not found, 200 = exists
Signup Form Selectors (Misskey/Sharkey SPA)
The form uses NO name attributes on inputs. Use type-based selectors:
Username: input[type="text"]
Email: input[type="email"]
Password: [REDACTED]"password"] (first)
Confirm: input[type="password"] (second)
The form is inside a modal with hash #mkmodal or #/signup. The SPA uses Vue/React — native DOM events may not trigger framework state updates. Use Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(el, value) + dispatchEvent(new Event('input', {bubbles: true})).
Turnstile Behavior
- Local Chrome (CDP): Turnstile loads in a separate Cloudflare challenge page/target. The widget may auto-solve after the checkbox is clicked.
- Browserbase (cloud browser): Turnstile iframe appears but has empty
src— widget does NOT load. Cloudflare blocks cloud browser IPs. - nodriver: Works but may fail to connect to Chrome on Windows. Use explicit
browser_executable_path. - CDP direct (recommended): Most reliable. Launch Chrome with
--remote-debugging-port, connect via WebSocket, usePage.navigate,Runtime.evaluate.
Registration Flow
- Navigate to
https://dc.hhhl.cc/ - Click "Присоединяйтесь к этому серверу" button
- Check two checkboxes (terms agreement)
- Click "Продолжить" (disabled until checkboxes checked)
- Handle "Подтвердить" confirmation dialogs (up to 2)
- Fill signup form: username, email, password ×2
- Solve Turnstile (manual or auto)
- Click "Начать" submit button
- Check response: if URL changes from
#mkmodal→ success
Silent Registration Failure
Critical: The SPA may close the dialog and return to the main page WITHOUT an error message. This looks like success but the account is never created. Always verify:
# Check if user exists (ActivityPub)
curl -s -H "Accept: application/activity+json" "https://dc.hhhl.cc/@username"
# 404 = registration failed silently
# 200 = account exists
# Better: Misskey API (JSON, more reliable)
curl -s -X POST "https://dc.hhhl.cc/api/users/show" \
-H "Content-Type: application/json" \
-d '{"username":"username_here"}'
# 200 + {"id":"..."} = account exists
# 404 + {"error":{"code":"NO_SUCH_USER"}} = failed
Pitfall: #mkmodal URL is NOT a success indicator — The modal URL appears even when registration silently fails (Turnstile token rejected). The CDP script autoreg_v4.py used #mkmodal as a success check — all 10 accounts were fake (NO_SUCH_USER). Always verify via API, never trust the URL alone.
Pitfall: #/signup in URL = definitely failed — if the page stays on the signup URL after submit, registration was rejected.
Python 3.14 Incompatibility
greenlet C extension is broken on Python 3.14 — This affects nodriver, patchright, and playwright. All three fail with ModuleNotFoundError: No module named 'greenlet._greenlet'.
Fix: Use Python 3.11 at /c/Users/User/AppData/Local/Python/Python311/python.exe. Working versions:
- Python 3.11.9 ✅ (greenlet, patchright, playwright all work)
- Python 3.13 ✅ (available but untested for Turnstile)
- Python 3.14 ❌ (greenlet broken)
Turnstile Bypass Attempts — All Failed on dc.hhhl.cc
Cloudflare Turnstile on dc.hhhl.cc (sitekey 0x4AAAAAADUhQ0UI_7UTXChP) blocks ALL automated approaches tested:
| Approach | Result | Notes |
|---|---|---|
| CDP (Chrome DevTools Protocol) | ❌ Blocked | Turnstile never auto-solves, submit button stays disabled |
| Browserbase (cloud browser) | ❌ Blocked | Turnstile iframe has empty src — widget doesn't load |
| patchright (Playwright anti-detection) | ❌ Blocked | Turnstile checkbox clickable but never auto-solves |
| Theyka Turnstile-Solver | ❌ Domain mismatch | Solver generates token on localhost — token invalid for dc.hhhl.cc |
Only working path: manual Turnstile solve in a real browser, then extract the cf-turnstile-response token from the DOM. Or use a paid service (capsolver, 2captcha) that can generate domain-valid tokens.
Turnstile-Solver domain-binding note: The Theyka solver (github.com/Theyka/Turnstile-Solver) uses page.route() to intercept the target URL and serve a local page with the Turnstile widget. The token is generated for localhost, not the target domain. Cloudflare rejects it. This pattern is NOT suitable for actual site registration — only for testing the solver itself.
Vue/Sharkey Form Interaction Pattern
The Sharkey (Misskey fork) SPA uses Vue.js. Standard Playwright/CDP clicks on checkboxes and buttons are silently ignored by the Vue reactivity system. JS evaluation + dispatchEvent is required:
// Check checkboxes and click Continue (all in one JS call)
(() => {
const cbs = document.querySelectorAll('input[type="checkbox"]');
cbs.forEach(cb => {
cb.checked = true;
cb.dispatchEvent(new Event('change', {bubbles: true}));
cb.dispatchEvent(new Event('input', {bubbles: true}));
});
const btns = document.querySelectorAll('button');
for (const btn of btns) {
if (btn.textContent.includes('Продолжить')) {
btn.disabled = false;
btn.click();
return 'clicked';
}
}
})()
Form filling also needs JS bypass:
const el = document.querySelector('input[name="username"]');
el.focus();
el.value = 'my_username';
el.dispatchEvent(new Event('input', {bubbles: true}));
Turnstile checkbox click (inside cross-origin iframe):
const frames = document.querySelectorAll('iframe');
for (const frame of frames) {
try {
const doc = frame.contentDocument || frame.contentWindow.document;
const cb = doc.querySelector('input[type="checkbox"]');
if (cb) { cb.click(); break; }
} catch(e) {}
}
Verification Email Problem
Short-term Gmail via AnyMessage cannot read spam folder. Verification emails from Misskey go to Gmail spam. Solutions:
- Use long-term email with IMAP (but AnyMessage long-term has no Gmail)
- Use external Gmail PVA service with IMAP/app-password
- Check spam via
[REDACTED-NAME]@gmail.comIMAP (if forwarded)
CDP Launch Pattern
CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
CDP_PORT = 9225
# Kill existing Chrome on port first
subprocess.run(f'for /f "tokens=5" %a in (\'netstat -ano ^| findstr :{CDP_PORT}\') do taskkill /F /PID %a',
shell=True, capture_output=True, timeout=5)
subprocess.run('taskkill /F /IM chrome.exe 2>nul', shell=True, capture_output=True, timeout=5)
time.sleep(2)
cmd = [
CHROME,
f"--remote-debugging-port={CDP_PORT}",
f"--user-data-dir={profile_dir}",
"--no-sandbox", "--disable-gpu",
"--no-first-run", "--no-default-browser-check",
"--no-restore-last-session", # CRITICAL: prevents Chrome from restoring previous tabs
"--disable-blink-features=AutomationControlled",
"--window-size=1280,900",
"--new-window",
"about:blank",
]
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(4)
CRITICAL: Browser-level vs Page-level WebSocket
When connecting to CDP, there are TWO WebSocket endpoints:
- Browser-level (
/json/version→webSocketDebuggerUrl): Only supportsTarget.*methods (createTarget, closeTarget, etc.) - Page-level (
/json→ page'swebSocketDebuggerUrl): SupportsPage.*,Runtime.*,DOM.*
Calling Page.enable on the browser-level WS returns "'Page.enable' wasn't found" (error -32601).
Correct pattern:
# 1. Connect to browser-level WS
browser_ws = GET /json/version → data["webSocketDebuggerUrl"]
browser_cdp = connect(browser_ws)
# 2. Create a new tab (browser-level)
result = await browser_cdp.cmd("Target.createTarget", {"url": "about:blank"})
target_id = result["targetId"]
# 3. Close browser connection, open page connection
await browser_cdp.close()
# 4. Find the new page's WS URL
pages = GET /json
page_ws = next(p["webSocketDebuggerUrl"] for p in pages if p["id"] == target_id)
# 5. Connect to page-level WS — NOW you can call Page.enable, Runtime.enable
page_cdp = connect(page_ws)
await page_cdp.cmd("Page.enable")
await page_cdp.cmd("Runtime.enable")
Chrome --no-restore-last-session Flag
Without this flag, Chrome restores the previous session's tabs and ignores the URL argument. The browser opens on a chrome-extension page or previous session's tabs instead of the intended URL. This caused manual_turnstile.py to hang on chrome-extension://nkeimhogjdpnpccoofpliimaahmaaome/background for 180 seconds.
Turnstile Token Polling + form.submit() Pattern
After the user solves Turnstile manually, the token appears in input[name="cf-turnstile-response"]. Poll at 1-second intervals (not 2s — faster detection reduces token expiry risk):
token = None
t0 = time.time()
while time.time() - t0 < 180:
token = await cdp.js("""
(function(){
var el=document.querySelector('input[name="cf-turnstile-response"]');
if(el&&el.value&&el.value.length>20)return el.value;
return '';
})()
""")
if isinstance(token, str) and len(token) > 20:
break
await asyncio.sleep(1) # 1s polling
# IMMEDIATE submit via form.submit() — faster than button.click()
await cdp.js("""
(function(){
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
if (forms[i].querySelector('input[name="username"]')) {
forms[i].submit();
return 'submitted';
}
}
// Fallback
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
if (btns[i].textContent.includes('Начать')) {
btns[i].click();
return 'clicked';
}
}
return 'not found';
})()
""")
Why form.submit() over button.click(): The button click triggers Vue/React event handlers which may add delays. form.submit() sends the form data directly, reducing the window between token detection and server submission. This matters because the Turnstile token may have a short validity window.
API Verification with Retries
Misskey is slow to index new users. POST /api/users/show may return NO_SUCH_USER for 10-30 seconds after successful registration. Retry pattern:
for attempt in range(8):
ok, info = verify_api(username)
if ok:
break
await asyncio.sleep(3) # 3s between retries
Gmail IMAP UTF-7 Folder Names (Russian)
Russian Gmail accounts use UTF-7 encoded folder names:
- Spam:
[Gmail]/&BCEEPwQwBDw-(= "Спам") - All Mail:
[Gmail]/&BBIEQQRP- &BD8EPgRHBEIEMA-(= "Вся почта") - Sent:
[Gmail]/&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-(= "Отправленные") - Trash:
[Gmail]/&BBoEPgRABDcEOAQ9BDA-(= "Корзина")
Use mail.list() to discover actual folder names, then use the encoded names in mail.select().