diff --git a/dist/hpl-toolbox-0.1.2.exe b/dist/hpl-toolbox-0.1.2.exe deleted file mode 100644 index dfb28a6..0000000 Binary files a/dist/hpl-toolbox-0.1.2.exe and /dev/null differ diff --git a/dist/hpl-toolbox-0.1.2.vsix b/dist/hpl-toolbox-0.1.2.vsix deleted file mode 100644 index 4be0719..0000000 Binary files a/dist/hpl-toolbox-0.1.2.vsix and /dev/null differ diff --git a/dist/hpl-toolbox-0.1.3.exe b/dist/hpl-toolbox-0.1.3.exe index c2736ec..7381e12 100644 Binary files a/dist/hpl-toolbox-0.1.3.exe and b/dist/hpl-toolbox-0.1.3.exe differ diff --git a/dist/hpl-toolbox-0.1.3.vsix b/dist/hpl-toolbox-0.1.3.vsix index e34dadc..26e270e 100644 Binary files a/dist/hpl-toolbox-0.1.3.vsix and b/dist/hpl-toolbox-0.1.3.vsix differ diff --git a/package.json b/package.json index cd68cac..76b8f02 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,17 @@ "type": "string", "default": "", "description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)." + }, + "hplToolbox.applovin.delayMs": { + "type": "number", + "default": 5000, + "minimum": 0, + "description": "Delay (in milliseconds) between consecutive file uploads, to avoid AppLovin rate limiting / 403 responses." + }, + "hplToolbox.applovin.randomizeUserAgent": { + "type": "boolean", + "default": true, + "description": "Randomize the User-Agent header on every request to p.applov.in to reduce bot detection." } } }, diff --git a/src/tools/applovinUpload.ts b/src/tools/applovinUpload.ts index 9f0e447..f54b5f9 100644 --- a/src/tools/applovinUpload.ts +++ b/src/tools/applovinUpload.ts @@ -50,9 +50,30 @@ export function openApplovinUpload(_context: vscode.ExtensionContext) { }); } +const USER_AGENTS = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', +]; + +function pickUserAgent(randomize: boolean): string { + if (!randomize) return USER_AGENTS[0]; + return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin'); const cookie = (cfg.get('cookie') || '').trim(); + const delayMs = Math.max(0, cfg.get('delayMs') ?? 1500); + const randomizeUA = cfg.get('randomizeUserAgent') ?? true; const paths = Array.isArray(filePaths) ? filePaths : [filePaths]; if (!paths.length) { @@ -60,14 +81,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { return; } - const headers: Record = { - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Origin': 'https://p.applov.in', - 'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1', - 'X-Requested-With': 'XMLHttpRequest', - 'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)', + const buildHeaders = (): Record => { + const headers: Record = { + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Origin': 'https://p.applov.in', + 'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1', + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': pickUserAgent(randomizeUA), + }; + if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`; + return headers; }; - if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`; const buildForm = (filePath: string): FormData => { const buf = fs.readFileSync(filePath); @@ -90,12 +114,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; } if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; } + if (i > 0 && delayMs > 0) { + panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Waiting ${delayMs}ms before ${fileName}...` }); + await sleep(delayMs); + } + panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` }); let res: Response; try { res = await fetch('https://p.applov.in/validateHTMLContent', { - method: 'POST', body: buildForm(filePath), headers, + method: 'POST', body: buildForm(filePath), headers: buildHeaders(), }); } catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; } const validateText = await res.text(); @@ -110,7 +139,7 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { try { res = await fetch('https://p.applov.in/getCachedAdURL', { - method: 'POST', body: buildForm(filePath), headers, + method: 'POST', body: buildForm(filePath), headers: buildHeaders(), }); } catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; } const cacheText = await res.text(); diff --git a/standalone/applovin.go b/standalone/applovin.go index 74d40b2..1f50b21 100644 --- a/standalone/applovin.go +++ b/standalone/applovin.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "mime/multipart" "net/http" urlpkg "net/url" @@ -12,8 +13,26 @@ import ( "path/filepath" "regexp" "strings" + "time" ) +var applovinUserAgents = []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", +} + +func pickApplovinUserAgent(randomize bool) string { + if !randomize { + return applovinUserAgents[0] + } + return applovinUserAgents[rand.Intn(len(applovinUserAgents))] +} + func ApplovinPage(w http.ResponseWriter, r *http.Request) { body := `

AppLovin Playable Preview (QR)

@@ -237,7 +256,16 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - cookie := LoadConfig().Applovin.Cookie + cfg := LoadConfig().Applovin + cookie := cfg.Cookie + delayMs := cfg.DelayMs + if delayMs < 0 { + delayMs = 0 + } + randomizeUA := true + if cfg.RandomizeUserAgent != nil { + randomizeUA = *cfg.RandomizeUserAgent + } w.Header().Set("Content-Type", "application/x-ndjson") flusher, _ := w.(http.Flusher) @@ -282,7 +310,7 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) { req.Header.Set("Origin", "https://p.applov.in") req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1") req.Header.Set("X-Requested-With", "XMLHttpRequest") - req.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") + req.Header.Set("User-Agent", pickApplovinUserAgent(randomizeUA)) if cookie != "" { req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie) } @@ -311,6 +339,11 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) { continue } + if i > 0 && delayMs > 0 { + send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Waiting %dms before %s...", idx, len(req.Paths), delayMs, fileName)}) + time.Sleep(time.Duration(delayMs) * time.Millisecond) + } + send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)}) body, ct, err := buildForm(filePath) diff --git a/standalone/hpltoolbox.exe b/standalone/hpltoolbox.exe index 836bf2d..7b17879 100644 Binary files a/standalone/hpltoolbox.exe and b/standalone/hpltoolbox.exe differ diff --git a/standalone/main.go b/standalone/main.go index 3810c4d..3d49e96 100644 --- a/standalone/main.go +++ b/standalone/main.go @@ -19,7 +19,7 @@ import ( var currentHwnd atomic.Uintptr func setupFileLogging() { - logPath := filepath.Join(appDir(), "debug.log") + logPath := filepath.Join(userDataDir(), "debug.log") f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return @@ -57,9 +57,16 @@ func main() { } defer removeLockFile() + webviewTmp, err := os.MkdirTemp("", "hpltoolbox-wv2-*") + if err != nil { + log.Fatalf("Failed to create webview temp dir: %v", err) + } + defer os.RemoveAll(webviewTmp) + w := webview.NewWithOptions(webview.WebViewOptions{ Debug: false, AutoFocus: true, + DataPath: webviewTmp, WindowOptions: webview.WindowOptions{ Title: "HPL Toolbox", Width: 1100, diff --git a/standalone/platform.go b/standalone/platform.go index 3a75324..5bc140d 100644 --- a/standalone/platform.go +++ b/standalone/platform.go @@ -18,7 +18,9 @@ type PlecConfig struct { } type ApplovinConfig struct { - Cookie string `json:"cookie"` + Cookie string `json:"cookie"` + DelayMs int `json:"delayMs"` + RandomizeUserAgent *bool `json:"randomizeUserAgent,omitempty"` } type SendToMobileConfig struct { @@ -38,6 +40,23 @@ type AppConfig struct { var configMu sync.Mutex +// userDataDir returns %LOCALAPPDATA%\HPLToolbox, creating it if needed. +// All mutable app files (config, lock, logs, WebView2 cache) live here so +// they are never placed next to the executable, which may be on a synced or +// antivirus-watched drive and would cause message-loop stalls. +func userDataDir() string { + local := os.Getenv("LOCALAPPDATA") + if local == "" { + local = os.Getenv("APPDATA") + } + if local == "" { + return appDir() + } + dir := filepath.Join(local, "HPLToolbox") + _ = os.MkdirAll(dir, 0755) + return dir +} + func defaultConfig() AppConfig { return AppConfig{ Plec: PlecConfig{ @@ -46,7 +65,7 @@ func defaultConfig() AppConfig { PreviewBase: "https://playable.applovindemo.com/phaser/", SkipExistsCheck: false, }, - Applovin: ApplovinConfig{Cookie: ""}, + Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000}, SendToMobile: SendToMobileConfig{Port: 0}, DailyUpdate: DailyUpdateConfig{LastProject: ""}, } @@ -72,7 +91,7 @@ func appDir() string { } func configPath() string { - return filepath.Join(appDir(), "config.json") + return filepath.Join(userDataDir(), "config.json") } func LoadConfig() AppConfig { diff --git a/standalone/single_instance.go b/standalone/single_instance.go index 24107c4..6d63882 100644 --- a/standalone/single_instance.go +++ b/standalone/single_instance.go @@ -12,7 +12,7 @@ import ( ) func lockFilePath() string { - return filepath.Join(appDir(), ".hpltoolbox.lock") + return filepath.Join(userDataDir(), ".hpltoolbox.lock") } // focusExistingInstance returns true if another instance was found and