playworks

This commit is contained in:
2026-05-28 11:47:26 +08:00
parent 274bafe1cb
commit 4f17523f26
4 changed files with 169 additions and 36 deletions

View File

@@ -77,10 +77,9 @@ func PlayworksPage(w http.ResponseWriter, r *http.Request) {
<div class="net-grid">
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="un" checked /><span class="net-label">Unity <span class="net-tag">un</span></span><span class="net-note">HTML (window.top to self)</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML + FbPlayableAd CTA</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
@@ -230,6 +229,11 @@ func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
}
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`)
var playworksMraidScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*</script>\s*`)
var playworksExitapiScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*</script>\s*`)
var playworksHeadCloseRx = regexp.MustCompile(`(?i)</head>`)
var playworksBodyOpenRx = regexp.MustCompile(`(?i)<body\b[^>]*>`)
var playworksBodyTagRx = regexp.MustCompile(`(?i)<body\b([^>]*)>`)
func playworksSourceBaseName(sourcePath string) string {
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
@@ -259,6 +263,9 @@ func playworksOutputBaseName(opts playworksConvertOptions) string {
}
func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
if !playworksIsSupportedNetwork(network) {
return "", fmt.Errorf("unsupported Playworks target network: %s", network)
}
transformed := transformPlayworksHTML(htmlSrc, network)
baseName := playworksOutputBaseName(opts)
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
@@ -288,8 +295,6 @@ func playworksNetworkOutputFolder(network string) string {
return "Applovin"
case "is":
return "Ironsource"
case "un":
return "Unity"
case "fb":
return "Facebook"
case "gg":
@@ -311,33 +316,130 @@ func playworksNeedsZip(network string) bool {
return network == "gg" || network == "vu" || network == "mtg"
}
func playworksIsMraidNetwork(network string) bool {
return network == "al" || network == "is"
}
func playworksIsSupportedNetwork(network string) bool {
switch network {
case "al", "is", "fb", "gg", "mo", "vu", "mtg", "tt":
return true
default:
return false
}
}
func transformPlayworksHTML(htmlSrc, network string) string {
result := htmlSrc
result = removePlayworksNetworkScripts(result, network)
headInject := buildPlayworksLifecycleScript()
if network == "al" || network == "is" {
if playworksIsMraidNetwork(network) {
headInject += `<script src="mraid.js"></script>`
} else if network == "gg" {
headInject += `<script src="exitapi.js"></script>`
}
result = strings.Replace(result, "</head>", headInject+"\n</head>", 1)
result = injectPlayworksBeforeHeadClose(result, headInject)
if network == "vu" {
result = strings.Replace(result, "<body>", `<body>
<script>window.__VUNGLE__=true;</script>`, 1)
result = injectPlayworksAfterBodyOpen(result, `<script>window.__VUNGLE__=true;</script>`)
} else if network == "mtg" {
result = strings.Replace(result, "<body>", `<body onload="gameReady()">`, 1)
result = addPlayworksBodyOnload(result, "gameReady()")
} else if network == "tt" {
result = strings.Replace(result, "<body>", `<body>
<script>window.__TIKTOK__=true;</script>`, 1)
result = injectPlayworksAfterBodyOpen(result, `<script>window.__TIKTOK__=true;</script>`)
}
result = replacePlayworksCTAScript(result, network)
if network == "un" {
result = strings.ReplaceAll(result, "window.top", "window.self")
return result
}
func removePlayworksNetworkScripts(htmlSrc, network string) string {
result := htmlSrc
if playworksIsMraidNetwork(network) {
result = playworksMraidScriptRx.ReplaceAllString(result, "")
}
if network == "gg" {
result = playworksExitapiScriptRx.ReplaceAllString(result, "")
}
return result
}
func injectPlayworksBeforeHeadClose(htmlSrc, injection string) string {
if playworksHeadCloseRx.MatchString(htmlSrc) {
return playworksHeadCloseRx.ReplaceAllString(htmlSrc, injection+"\n</head>")
}
return injection + "\n" + htmlSrc
}
func injectPlayworksAfterBodyOpen(htmlSrc, injection string) string {
loc := playworksBodyOpenRx.FindStringIndex(htmlSrc)
if loc == nil {
return injection + "\n" + htmlSrc
}
return htmlSrc[:loc[1]] + "\n" + injection + htmlSrc[loc[1]:]
}
func addPlayworksBodyOnload(htmlSrc, handler string) string {
loc := playworksBodyTagRx.FindStringSubmatchIndex(htmlSrc)
if loc == nil {
return `<body onload="` + handler + `">` + "\n" + htmlSrc
}
attrs := htmlSrc[loc[2]:loc[3]]
onloadStart, onloadEnd, quote, valueStart, valueEnd := findPlayworksOnloadAttr(attrs)
if onloadStart == -1 {
updated := `<body` + attrs + ` onload="` + handler + `">`
return htmlSrc[:loc[0]] + updated + htmlSrc[loc[1]:]
}
value := attrs[valueStart:valueEnd]
if strings.Contains(value, handler) {
return htmlSrc
}
replacement := `onload=` + quote + value + `;` + handler + quote
updatedAttrs := attrs[:onloadStart] + replacement + attrs[onloadEnd:]
updatedTag := `<body` + updatedAttrs + `>`
return htmlSrc[:loc[0]] + updatedTag + htmlSrc[loc[1]:]
}
func findPlayworksOnloadAttr(attrs string) (start int, end int, quote string, valueStart int, valueEnd int) {
lower := strings.ToLower(attrs)
idx := strings.Index(lower, "onload")
for idx != -1 {
if idx > 0 {
prev := lower[idx-1]
if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == '-' || prev == '_' {
next := strings.Index(lower[idx+len("onload"):], "onload")
if next == -1 {
return -1, -1, "", -1, -1
}
idx += len("onload") + next
continue
}
}
pos := idx + len("onload")
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
pos++
}
if pos >= len(attrs) || attrs[pos] != '=' {
return -1, -1, "", -1, -1
}
pos++
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
pos++
}
if pos >= len(attrs) || (attrs[pos] != '"' && attrs[pos] != '\'') {
return -1, -1, "", -1, -1
}
q := attrs[pos]
valueStart = pos + 1
valueEnd = strings.IndexByte(attrs[valueStart:], q)
if valueEnd == -1 {
return -1, -1, "", -1, -1
}
valueEnd += valueStart
return idx, valueEnd + 1, string(q), valueStart, valueEnd
}
return -1, -1, "", -1, -1
}
func replacePlayworksCTAScript(htmlSrc, network string) string {
marker := "Luna.Unity.Playable.InstallFullGame=function"
markerIdx := strings.LastIndex(htmlSrc, marker)
@@ -393,15 +495,13 @@ func buildPlayworksCTAScript(network string) string {
ctaLogic := closeCall + `window.open(o,"_blank");`
switch network {
case "al", "is":
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`
case "un":
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`
case "fb":
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
case "gg":
ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`
case "mo":
ctaLogic = closeCall + `if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
case "vu":
ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
case "mtg":