package main
import (
"archive/zip"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
type playworksConvertOptions struct {
SourcePath string `json:"sourcePath"`
OutputDir string `json:"outputDir"`
Networks []string `json:"networks"`
}
type playworksNetworkResult struct {
Network string `json:"network"`
File string `json:"file"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
}
func PlayworksPage(w http.ResponseWriter, r *http.Request) {
body := `
Playworks Converter
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(Page("/playworks", "Playworks Converter", body)))
}
func PlayworksPickSource(w http.ResponseWriter, r *http.Request) {
picked := PickFiles("Select Playworks HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, false, "")
if len(picked) == 0 {
writeJSON(w, map[string]any{})
return
}
writeJSON(w, map[string]any{"path": picked[0], "dir": filepath.Dir(picked[0])})
}
func PlayworksPickOutput(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"path": PickFolder("Select Output Folder", "")})
}
func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
var opts playworksConvertOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": err.Error()})
return
}
if !fileExists(opts.SourcePath) {
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Source file not found."})
return
}
if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not create output folder."})
return
}
data, err := os.ReadFile(opts.SourcePath)
if err != nil {
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not read source: " + err.Error()})
return
}
results := []playworksNetworkResult{}
for _, network := range opts.Networks {
filePath, err := convertPlayworksNetwork(string(data), opts, network)
if err != nil {
results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()})
continue
}
results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true})
}
writeJSON(w, map[string]any{"results": results})
}
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`)
func playworksSourceBaseName(sourcePath string) string {
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
return playworksSuffixRx.ReplaceAllString(base, "")
}
func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
transformed := transformPlayworksHTML(htmlSrc, network)
baseName := playworksSourceBaseName(opts.SourcePath)
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
outputDir := filepath.Join(opts.OutputDir, playworksNetworkOutputFolder(network))
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", err
}
if playworksNeedsZip(network) {
zipPath := filepath.Join(outputDir, fmt.Sprintf("%s_%s.zip", baseName, network))
if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil {
return "", err
}
return zipPath, nil
}
outPath := filepath.Join(outputDir, htmlFileName)
if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil {
return "", err
}
return outPath, nil
}
func playworksNetworkOutputFolder(network string) string {
switch network {
case "al":
return "Applovin"
case "is":
return "Ironsource"
case "un":
return "Unity"
case "fb":
return "Facebook"
case "gg":
return "GoogleAds"
case "mo":
return "Moloco"
case "vu":
return "Vungle"
case "mtg":
return "Mintegral"
case "tt":
return "TikTok"
default:
return network
}
}
func playworksNeedsZip(network string) bool {
return network == "gg" || network == "vu" || network == "mtg"
}
func transformPlayworksHTML(htmlSrc, network string) string {
result := htmlSrc
headInject := buildPlayworksLifecycleScript()
if network == "al" || network == "is" {
headInject += ``
} else if network == "gg" {
headInject += ``
}
result = strings.Replace(result, "", headInject+"\n", 1)
if network == "vu" {
result = strings.Replace(result, "", `
`, 1)
} else if network == "mtg" {
result = strings.Replace(result, "", ``, 1)
} else if network == "tt" {
result = strings.Replace(result, "", `
`, 1)
}
result = replacePlayworksCTAScript(result, network)
if network == "un" {
result = strings.ReplaceAll(result, "window.top", "window.self")
}
return result
}
func replacePlayworksCTAScript(htmlSrc, network string) string {
marker := "Luna.Unity.Playable.InstallFullGame=function"
markerIdx := strings.LastIndex(htmlSrc, marker)
if markerIdx == -1 {
return htmlSrc
}
scriptOpenIdx := strings.LastIndex(htmlSrc[:markerIdx], "")
if scriptCloseRel == -1 {
return htmlSrc
}
scriptCloseIdx := markerIdx + scriptCloseRel
return htmlSrc[:scriptOpenIdx] + buildPlayworksConsoleRestoreScript() + buildPlayworksCTAScript(network) + htmlSrc[scriptCloseIdx+len(""):]
}
func buildPlayworksConsoleRestoreScript() string {
return strings.Join([]string{
``,
}, "")
}
func buildPlayworksLifecycleScript() string {
return strings.Join([]string{
``,
}, "")
}
func buildPlayworksCTAScript(network string) string {
urlSetup := `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`
closeCall := `try{window.gameClose();}catch(e){}`
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");}`
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");`
case "vu":
ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
case "mtg":
ctaLogic = closeCall + `if(typeof window.install==="function"){window.install();return;}window.open(o,"_blank");`
case "tt":
ctaLogic = closeCall + `if(typeof window.openAppStore==="function"){window.openAppStore();return;}window.open(o,"_blank");`
}
return ``
}
func createSingleFileZip(data []byte, nameInZip, destZipPath string) error {
_ = os.Remove(destZipPath)
out, err := os.Create(destZipPath)
if err != nil {
return err
}
defer out.Close()
zw := zip.NewWriter(out)
entry, err := zw.Create(nameInZip)
if err != nil {
_ = zw.Close()
return err
}
if _, err := entry.Write(data); err != nil {
_ = zw.Close()
return err
}
return zw.Close()
}