diff --git a/scripts/misc/twitch-autoreg.ts b/scripts/misc/twitch-autoreg.ts new file mode 100644 index 0000000..37d82e1 --- /dev/null +++ b/scripts/misc/twitch-autoreg.ts @@ -0,0 +1,365 @@ +import type { Browser } from 'patchright' +import type { EmailVerificationProvider } from '../../utils/temkakit/email-verification.ts' +import { writeFile } from 'node:fs/promises' +import { faker } from '@faker-js/faker' +import { sleep } from '@fuman/utils' +import { load } from 'cheerio' +import { Cookie, CookieJar } from 'tough-cookie' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { AnymessageEmailVerificationProvider } from '../../utils/temkakit/anymessage.ts' +import { solveKasadaSalamoonder } from '../../utils/temkakit/kasada-solver.ts' +import { createLibcurlFetch } from '../../utils/temkakit/libcurl.ts' + +// half broken, unfinished + +function getProxy() { + // return { + // user: 'JaTjXK', + // pass: 'WYsU4C', + // host: '38.152.247.16', + // port: 9785, + // } + return { + user: '', + pass: '', + host: '127.0.0.1', + port: 7891, + } +} + +function proxyToUrl(proxy: { user: string, pass: string, host: string, port: number }) { + return `http://${proxy.user}:${proxy.pass}@${proxy.host}:${proxy.port}` +} + +const THREADS = 1 +const ACCOUNTS_COUNT = 2 + +const TWITCH_PJS = 'https://k.twitchcdn.net/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/p.js' + +async function twitchAutoreg(options: { + // browser: Browser + emailProvider: EmailVerificationProvider + log?: (format: string, ...args: any[]) => void + proxy?: string +}) { + const { + // browser, + proxy, + emailProvider, + log = (fmt, ...args) => console.log(fmt, ...args), + } = options + const jar = new CookieJar() + + log('proxy', proxy) + const ffetch = ffetchBase.extend({ + cookies: jar, + fetch: createLibcurlFetch({ proxy }), + }) + + log('fetching main page') + const mainPage = await ffetch('https://www.twitch.tv/').text() + const twilightBuildId = mainPage.match(/window.__twilightBuildID="([^"]+)"/)?.[1] + if (!twilightBuildId) { + throw new Error('failed to get twilightBuildId') + } + + await jar.setCookie(new Cookie({ + key: 'api_token', + value: `twilight.${faker.string.hexadecimal({ length: 32 })}`, + domain: 'twitch.tv', + path: '/', + secure: true, + sameSite: 'None', + hostOnly: false, + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }), 'https://www.twitch.tv') + await jar.setCookie(new Cookie({ + key: 'experiment_overrides', + value: encodeURIComponent(JSON.stringify({ experiments: {}, disabled: [] })), + domain: 'twitch.tv', + path: '/', + secure: true, + sameSite: 'None', + hostOnly: false, + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }), 'https://www.twitch.tv') + + const deviceId = faker.string.alphanumeric({ length: 32 }) + const sessionId = faker.string.hexadecimal({ length: 16 }) + + log('generating integrity token') + + // const kasadaSolver = await createKasadaSolver({ + // pageUrl: 'https://www.twitch.tv/', + // scriptUrl: '', + // browser, + // beforePageLoad: async (page) => { + // await syncCookiesIntoBrowser(jar, page.context()) + // }, + // requests: [ + // { + // protocol: 'https', + // method: 'POST', + // domain: 'gql.twitch.tv', + // path: '/integrity', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/integrity', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/protected_register', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/protected_login', + // }, + // ], + // }) + + const commonHeaders: Record = { + 'X-Device-Id': deviceId, + 'Client-Id': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + 'Client-Request-Id': faker.string.alphanumeric({ length: 32 }), + 'Client-Session-Id': sessionId, + 'Client-Version': twilightBuildId, + } + + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + // const integrityToken = await kasadaSolver.request({ + // url: 'https://gql.twitch.tv/integrity', + // method: 'POST', + // headers: commonHeaders, + // }) as { token: string } + const integrityToken = await ffetch('https://gql.twitch.tv/integrity', { + method: 'POST', + headers: { + ...commonHeaders, + ...kasadaSolution, + }, + }).json() as { token: string } + + const ffetchGql = ffetchBase.extend({ + headers: { + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + ...commonHeaders, + 'Client-Integrity': integrityToken.token, + }, + }) + + // await syncCookiesFromBrowser(kasadaSolver.page.context(), jar) + + let username + while (true) { + username = faker.internet.username().toLowerCase().replace(/[^a-z0-9]/gi, '') + log('checking username', username) + const r = await ffetchGql.post('https://gql.twitch.tv/gql', { + json: [ + { + operationName: 'UsernameValidator_User', + variables: { username }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'fd1085cf8350e309b725cf8ca91cd90cac03909a3edeeedbd0872ac912f3d660', + }, + }, + }, + ], + }).json() as any + + if (r[0].errors) { + throw new Error(`failed to check username:${JSON.stringify(r[0].errors)}`) + } + + if (r[0].data.isUsernameAvailable) { + log('username is available: %s', username) + break + } + + await sleep(1000) + } + + log('ordering email') + + const email = await emailProvider.getEmail() + + log('got email: %s, registering', email) + + const password = faker.internet.password({ length: 16, pattern: /[a-z0-9]/ }) + const birthday = faker.date.birthdate({ min: 18, max: 25, mode: 'age' }) + const registerBody: Record = { + username, + password, + email, + birthday: { + day: birthday.getDate(), + month: birthday.getMonth() + 1, + year: birthday.getFullYear(), + isOver18: true, + }, + email_marketing_opt_in: false, + client_id: 'kimne78kx3ncx6brgo4mv6wki5h1ko', + is_password_guide: 'nist', + } + + for (let i = 0; i < 5; i++) { + // const r1 = await kasadaSolver.request({ + // url: 'https://passport.twitch.tv/protected_register', + // method: 'POST', + // body: JSON.stringify(registerBody), + // headers: { + // 'Content-Type': 'text/plain;charset=UTF-8', + // 'Accept': '*/*', + // }, + // credentials: 'include', + // }) as { error_code: number } + log('solving kasada') + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + const r1 = await ffetch.post('https://passport.twitch.tv/protected_register', { + validateResponse: false, + json: registerBody, + headers: { + ...kasadaSolution, + }, + }).json() as { error_code: number } + + log('r1', r1) + + if (i < 4 && r1.error_code === 5025) { + log('integrity failed, retrying...') + continue + } + + if (r1.error_code !== 2026) { + await emailProvider.dispose() + throw new Error(`failed to register: ${JSON.stringify(r1)}`) + } + + break + } + + log('waiting for code') + const message = await emailProvider.waitForMessage({ timeout: 90_000 }) + const message$ = load(message) + const code = message$('center p[style^=background]').text() // what the fuck is this selector + // const code = await question('code: ') + + if (!code.match(/^\d{6}$/)) { + log('❌ invalid code parsed: %s', code) + log(message) + await emailProvider.dispose() + throw new Error(`invalid code parsed:${code}`) + } + + log('code: %s', code) + + registerBody.email_verification_code = code + for (let i = 0; i < 5; i++) { + // const r2 = await kasadaSolver.request({ + // url: 'https://passport.twitch.tv/protected_register', + // method: 'POST', + // body: JSON.stringify(registerBody), + // headers: { + // 'Content-Type': 'text/plain;charset=UTF-8', + // }, + // credentials: 'include', + // }) as { error_code: number } + log('solving kasada') + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + const r2 = await ffetch.post('https://passport.twitch.tv/protected_register', { + json: registerBody, + validateResponse: false, + headers: { + ...kasadaSolution, + }, + }).json() as { error_code: number } + + if (i < 4 && r2.error_code === 5025) { + log('integrity failed, retrying...') + continue + } + + if (r2.error_code) { + await emailProvider.dispose() + throw new Error(`❌ failed to register:${r2.error_code}`) + } + + break + } + + // await syncCookiesFromBrowser(kasadaSolver.page.context(), jar) + + log('авторег работает!') + await emailProvider.dispose() + + return { + username, + password, + email, + cookies: await jar.store.getAllCookies(), + } +} + +let started = 0 +let completed = 0 +await Promise.all(Array.from({ length: THREADS }).map(async (_, idx) => { + const emailProvider = new AnymessageEmailVerificationProvider({ + site: 'twitch.tv', + domain: 'hotmail.com', + }) + + let browser: Browser | null = null + while (true) { + if (started >= ACCOUNTS_COUNT) { + break + } + + started++ + + const log = (fmt: string, ...args: any[]) => console.log(`[worker ${idx}] ${fmt}`, ...args) + + try { + const proxy = getProxy() + + // browser = await chromium.launch({ + // channel: 'chrome', + // headless: false, + // env: { + // TZ: 'Europe/Amsterdam', + // }, + // proxy: { + // server: `http://${proxy.host}:${proxy.port}`, + // username: proxy.user, + // password: proxy.pass, + // }, + // }) + + const acct = await twitchAutoreg({ + // browser, + proxy: proxyToUrl(proxy), + emailProvider, + log, + }) + + await writeFile('assets/twitch-accs.txt', `${JSON.stringify(acct)}\n`, { flag: 'a' }) + + completed++ + log('completed: %d/%d', completed, ACCOUNTS_COUNT) + } catch (e) { + log('autoreg error: %s', e) + // await browser?.close() + browser = null + started-- + } + } +}))