287 lines
12 KiB
Markdown
287 lines
12 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
1. **Local Chrome (CDP)**: Turnstile loads in a separate Cloudflare challenge page/target. The widget may auto-solve after the checkbox is clicked.
|
||
2. **Browserbase (cloud browser)**: Turnstile iframe appears but has empty `src` — widget does NOT load. Cloudflare blocks cloud browser IPs.
|
||
3. **nodriver**: Works but may fail to connect to Chrome on Windows. Use explicit `browser_executable_path`.
|
||
4. **CDP direct (recommended)**: Most reliable. Launch Chrome with `--remote-debugging-port`, connect via WebSocket, use `Page.navigate`, `Runtime.evaluate`.
|
||
|
||
## Registration Flow
|
||
|
||
1. Navigate to `https://dc.hhhl.cc/`
|
||
2. Click "Присоединяйтесь к этому серверу" button
|
||
3. Check two checkboxes (terms agreement)
|
||
4. Click "Продолжить" (disabled until checkboxes checked)
|
||
5. Handle "Подтвердить" confirmation dialogs (up to 2)
|
||
6. Fill signup form: username, email, password ×2
|
||
7. Solve Turnstile (manual or auto)
|
||
8. Click "Начать" submit button
|
||
9. 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:
|
||
|
||
```bash
|
||
# 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**:
|
||
|
||
```javascript
|
||
// 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**:
|
||
```javascript
|
||
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):
|
||
```javascript
|
||
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.com` IMAP (if forwarded)
|
||
|
||
## CDP Launch Pattern
|
||
|
||
```python
|
||
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 supports `Target.*` methods (createTarget, closeTarget, etc.)
|
||
- **Page-level** (`/json` → page's `webSocketDebuggerUrl`): Supports `Page.*`, `Runtime.*`, `DOM.*`
|
||
|
||
**Calling `Page.enable` on the browser-level WS returns `"'Page.enable' wasn't found"` (error -32601).**
|
||
|
||
Correct pattern:
|
||
```python
|
||
# 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):
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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()`. |