├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── archive.go ├── go.mod ├── go.sum ├── main.go ├── patcher.go └── util.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | pull_request: 7 | tags: 8 | - '*' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up Go 17 | uses: actions/checkout@v2 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: '1.20' 21 | check-latest: true 22 | - run: mkdir -p build/ 23 | 24 | - name: Build for Windows amd64 25 | env: 26 | GOPROXY: "https://proxy.golang.org" 27 | GOARCH: "amd64" 28 | GOOS: "windows" 29 | run: go build -o build/patcher.win-amd64.exe 30 | 31 | - name: Build for Mac amd64 32 | env: 33 | GOPROXY: "https://proxy.golang.org" 34 | GOARCH: "amd64" 35 | GOOS: "darwin" 36 | run: go build -o build/patcher.mac-amd64 37 | 38 | - name: Build for Mac arm64 39 | env: 40 | GOPROXY: "https://proxy.golang.org" 41 | GOARCH: "arm64" 42 | GOOS: "darwin" 43 | run: go build -o build/patcher.mac-arm64 44 | 45 | - name: Build for Linux amd64 46 | env: 47 | GOPROXY: "https://proxy.golang.org" 48 | GOARCH: "amd64" 49 | GOOS: "linux" 50 | run: go build -o build/patcher.linux-amd64 51 | 52 | - name: Build for Linux arm64 53 | env: 54 | GOPROXY: "https://proxy.golang.org" 55 | GOARCH: "arm64" 56 | GOOS: "linux" 57 | run: go build -o build/patcher.linux-arm64 58 | 59 | - name: Create release 60 | uses: softprops/action-gh-release@v1 61 | with: 62 | tag_name: release-${{ github.ref_name }} 63 | files: | 64 | build/patcher.win-amd64.exe 65 | build/patcher.mac-amd64 66 | build/patcher.mac-arm64 67 | build/patcher.linux-amd64 68 | build/patcher.linux-arm64 69 | LICENSE 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.ipa 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | 24 | # Patcher files 25 | Enmity.ipa 26 | Enmity -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | --- 6 | 7 |
8 |

Enmity Patcher

9 | 10 | Script used to patch Discord with a custom name and icon. It'll automatically fetch the latest version of Discord and the tweak for you.
11 | You can download the latest version [here](https://github.com/enmity-mod/enmity-patcher/releases/latest). 12 |
13 | 14 | --- 15 | 16 |
17 | Discord 18 | Twitter 19 |
20 | -------------------------------------------------------------------------------- /archive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/flate" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/mholt/archiver" 9 | ) 10 | 11 | func extract() { 12 | logger.Debugf("Attempting to extract \"%s\"", ipa) 13 | format := archiver.Zip{} 14 | directory = fileNameWithoutExtension(filepath.Base(ipa)) 15 | 16 | if _, err := os.Stat(ipa); err != nil { 17 | logger.Errorf("Couldn't find \"%s\". Does it exist?", ipa) 18 | exit() 19 | } 20 | 21 | if _, err := os.Stat(directory); err == nil { 22 | logger.Debug("Detected previously extracted directory, cleaning it up...") 23 | 24 | err := os.RemoveAll(directory) 25 | if err != nil { 26 | logger.Errorf("Failed to clean up previously extracted directory: %s", err) 27 | exit() 28 | } 29 | 30 | logger.Info("Previously extracted directory cleaned up. ") 31 | } 32 | 33 | err := format.Unarchive(ipa, directory) 34 | if err != nil { 35 | logger.Errorf("Failed to extract %s: **%v**", ipa, err) 36 | os.Exit(1) 37 | } 38 | 39 | logger.Infof("Successfully extracted to \"%s\"", directory) 40 | } 41 | 42 | func archive() { 43 | logger.Debugf("Attempting to archive \"%s\"", directory) 44 | 45 | format := archiver.Zip{CompressionLevel: flate.BestCompression} 46 | zip := directory + ".zip" 47 | 48 | if _, err := os.Stat(zip); err == nil { 49 | logger.Debug("Detected previous archive, cleaning it up...") 50 | 51 | err := os.Remove(zip) 52 | if err != nil { 53 | logger.Errorf("Failed to clean up previous archive: %s", err) 54 | exit() 55 | } 56 | 57 | logger.Info("Previous archive cleaned up.") 58 | } 59 | 60 | logger.Infof("Archiving \"%s\" to \"%s\"", directory, zip) 61 | err := format.Archive([]string{filepath.Join(directory, "Payload")}, zip) 62 | if err != nil { 63 | logger.Errorf("Failed to archive \"%s\": %v", zip, err) 64 | exit() 65 | } 66 | 67 | if _, err := os.Stat("Enmity.ipa"); err == nil { 68 | logger.Debug("Detected previous Enmity IPA, cleaning it up...") 69 | 70 | err := os.Remove("Enmity.ipa") 71 | if err != nil { 72 | logger.Errorf("Failed to clean up previous Enmity IPA: %s", err) 73 | exit() 74 | } 75 | 76 | logger.Info("Previous Enmity IPA cleaned up.") 77 | } 78 | 79 | err = os.Rename(zip, "Enmity.ipa") 80 | if err != nil { 81 | logger.Errorf("Failed to rename \"%s\": %v", zip, err) 82 | exit() 83 | } 84 | 85 | logger.Infof("Successfully archived \"%s\" to \"Enmity.ipa\"", zip) 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module enmity/patcher 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/charmbracelet/log v0.3.1 7 | github.com/mholt/archiver v3.1.1+incompatible 8 | github.com/urfave/cli/v2 v2.27.1 9 | howett.net/plist v1.0.1 10 | ) 11 | 12 | require ( 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/lipgloss v0.9.1 // indirect 15 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 16 | github.com/dsnet/compress v0.0.1 // indirect 17 | github.com/frankban/quicktest v1.14.6 // indirect 18 | github.com/go-logfmt/logfmt v0.6.0 // indirect 19 | github.com/golang/snappy v0.0.4 // indirect 20 | github.com/google/go-cmp v0.6.0 // indirect 21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/mattn/go-runewidth v0.0.15 // indirect 24 | github.com/muesli/reflow v0.3.0 // indirect 25 | github.com/muesli/termenv v0.15.2 // indirect 26 | github.com/nwaples/rardecode v1.1.3 // indirect 27 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 28 | github.com/rivo/uniseg v0.4.4 // indirect 29 | github.com/rogpeppe/go-internal v1.12.0 // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/ulikunitz/xz v0.5.11 // indirect 32 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 33 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect 34 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect 35 | golang.org/x/sys v0.16.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 | github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= 4 | github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= 5 | github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= 6 | github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= 12 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 13 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 14 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 15 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 16 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 17 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 18 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 19 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 20 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 22 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 23 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 24 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 25 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 26 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 27 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 28 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 31 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 32 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 33 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 34 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 35 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 36 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 37 | github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= 38 | github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= 39 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 40 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 41 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 42 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 43 | github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= 44 | github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= 45 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 46 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 47 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 50 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 51 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 52 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 53 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 54 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 55 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 56 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 57 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 59 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 60 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 61 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 62 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 63 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 64 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 65 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 66 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= 67 | github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 68 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= 69 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 72 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 76 | howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= 77 | howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 78 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/charmbracelet/log" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | var logger = log.NewWithOptions(os.Stderr, log.Options{ 12 | ReportCaller: false, 13 | ReportTimestamp: true, 14 | TimeFormat: time.TimeOnly, 15 | Level: log.DebugLevel, 16 | Prefix: "Patcher", 17 | }) 18 | 19 | var ( 20 | info map[string]interface{} 21 | directory string 22 | assets string 23 | ipa string 24 | ) 25 | 26 | func main() { 27 | app := &cli.App{ 28 | Name: "patcher-ios", 29 | Usage: "Patches the Discord IPA with icons, utilities and features to ease usability.", 30 | Action: func(context *cli.Context) error { 31 | ipa = context.Args().Get(0) 32 | 33 | if ipa == "" { 34 | logger.Error("Please provide a path to the IPA.") 35 | os.Exit(1) 36 | } 37 | 38 | logger.Infof("Requested IPA patch for \"%s\"", ipa) 39 | 40 | extract() 41 | loadInfo() 42 | 43 | setReactNavigationName() 44 | setSupportedDevices() 45 | setFileAccess() 46 | setAppName() 47 | setIcons() 48 | 49 | saveInfo() 50 | archive() 51 | 52 | exit() 53 | return nil 54 | }, 55 | } 56 | 57 | assets = os.TempDir() 58 | 59 | if err := app.Run(os.Args); err != nil { 60 | logger.Fatal(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /patcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/mholt/archiver" 11 | ) 12 | 13 | type Manifest struct { 14 | Metadata struct { 15 | Build string `json:"build"` 16 | Commit string `json:"commit"` 17 | ConfirmUpdate bool `json:"confirm_update"` 18 | } `json:"metadata"` 19 | Hashes map[string]string `json:"hashes"` 20 | } 21 | 22 | func setSupportedDevices() { 23 | logger.Debug("Setting supported devices...") 24 | 25 | delete(info, "UISupportedDevices") 26 | 27 | logger.Info("Supported devices set.") 28 | } 29 | 30 | func setAppName() { 31 | logger.Debug("Setting app name...") 32 | 33 | info["CFBundleName"] = "Enmity" 34 | info["CFBundleDisplayName"] = "Enmity" 35 | 36 | logger.Info("App name set.") 37 | } 38 | 39 | func setFileAccess() { 40 | logger.Debug("Setting file access...") 41 | 42 | info["UISupportsDocumentBrowser"] = true 43 | info["UIFileSharingEnabled"] = true 44 | 45 | logger.Info("File access enabled.") 46 | } 47 | 48 | func setIcons() { 49 | logger.Debug("Downloading app icons...") 50 | 51 | icons := filepath.Join(assets, "icons.zip") 52 | download("https://enmity-mod.github.io/assets/icons.zip", icons) 53 | 54 | logger.Info("Downloaded app icons.") 55 | 56 | logger.Debug("Applying app icons...") 57 | 58 | info["CFBundleIcons"].(map[string]interface{})["CFBundlePrimaryIcon"].(map[string]interface{})["CFBundleIconName"] = "EnmityIcon" 59 | info["CFBundleIcons"].(map[string]interface{})["CFBundlePrimaryIcon"].(map[string]interface{})["CFBundleIconFiles"] = []string{"EnmityIcon60x60"} 60 | info["CFBundleIcons~ipad"].(map[string]interface{})["CFBundlePrimaryIcon"].(map[string]interface{})["CFBundleIconName"] = "EnmityIcon" 61 | info["CFBundleIcons~ipad"].(map[string]interface{})["CFBundlePrimaryIcon"].(map[string]interface{})["CFBundleIconFiles"] = []string{"EnmityIcon60x60", "EnmityIcon76x76"} 62 | 63 | zip := archiver.Zip{OverwriteExisting: true} 64 | discord := filepath.Join(directory, "Payload", "Discord.app") 65 | 66 | if err := zip.Unarchive(icons, discord); err == nil { 67 | logger.Info("Applied app icons.") 68 | } else { 69 | logger.Errorf("Failed to apply app icons: %v", err) 70 | exit() 71 | } 72 | } 73 | 74 | func setReactNavigationName() { 75 | logger.Debug("Attempting to rename React Navigation...") 76 | modulesPath := filepath.Join(directory, "Payload", "Discord.app", "assets", "_node_modules", ".pnpm") 77 | 78 | if exists, _ := exists(modulesPath); !exists { 79 | logger.Debug("React Navigation does not exist, no need to rename it.") 80 | return 81 | } 82 | 83 | manifestPath := filepath.Join(directory, "Payload", "Discord.app", "manifest.json") 84 | 85 | if exists, _ := exists(manifestPath); !exists { 86 | logger.Debug("React Navigation does not exist, no need to rename it.") 87 | return 88 | } 89 | 90 | content, err := os.ReadFile(manifestPath) 91 | 92 | if err != nil { 93 | logger.Errorf("Couldn't find manifest.json inside the IPA payload. %v", err) 94 | exit() 95 | } 96 | 97 | manifest := Manifest{} 98 | if err := json.Unmarshal(content, &manifest); err != nil { 99 | logger.Errorf("Failed to unmarshal manifest.json. %v", err) 100 | exit() 101 | } 102 | 103 | if manifest.Hashes == nil { 104 | logger.Infof("No hashes found in manifest.json. Skipping React Navigation rename.") 105 | return 106 | } 107 | 108 | // Change manifest hash path 109 | for key := range manifest.Hashes { 110 | if !strings.Contains(key, "@react-navigation+elements") { 111 | continue 112 | } 113 | 114 | value := manifest.Hashes[key] 115 | split := strings.Split(key, "/") 116 | 117 | for idx, segment := range split { 118 | if !strings.Contains(segment, "@react-navigation+elements") { 119 | continue 120 | } 121 | 122 | split[idx] = "@react-navigation+elements@patched" 123 | } 124 | 125 | delete(manifest.Hashes, key) 126 | 127 | newKey := strings.Join(split, "/") 128 | manifest.Hashes[newKey] = value 129 | } 130 | 131 | content, err = json.Marshal(manifest) 132 | 133 | if err != nil { 134 | logger.Errorf("Failed to marshal modified manifest structure. %v", err) 135 | return 136 | } 137 | 138 | err = os.WriteFile(manifestPath, content, 0644) 139 | 140 | if err != nil { 141 | logger.Errorf("Failed to write modified manifest.json file. %v", err) 142 | return 143 | } else { 144 | logger.Info("Wrote modified manifest.json file.") 145 | } 146 | 147 | // Rename node_modules module folder(s) 148 | files, err := os.ReadDir(modulesPath) 149 | 150 | if err != nil { 151 | logger.Errorf("Failed to read node_modules directory. Skipping React Navigation rename. %v", err) 152 | return 153 | } 154 | 155 | directories := filter(files, func(entry fs.DirEntry) bool { 156 | return strings.Contains(entry.Name(), "@react-navigation+elements") 157 | }) 158 | 159 | for _, directory := range directories { 160 | currentName := filepath.Join(modulesPath, directory.Name()) 161 | newName := filepath.Join(modulesPath, "@react-navigation+elements@patched") 162 | 163 | if err := os.Rename(currentName, newName); err != nil { 164 | logger.Errorf("Failed to rename React Navigation directory: %v %v", directory.Name(), err) 165 | } else { 166 | logger.Infof("Renamed React Navigation directory: %v", directory.Name()) 167 | } 168 | } 169 | 170 | logger.Info("Successfully renamed React Navigation directories.") 171 | } 172 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | 9 | "howett.net/plist" 10 | ) 11 | 12 | func fileNameWithoutExtension(name string) string { 13 | return name[:len(name)-len(filepath.Ext(name))] 14 | } 15 | 16 | func exit() { 17 | logger.Debug("Cleaning up...") 18 | 19 | if _, e := os.Stat(directory); e == nil { 20 | if e := os.RemoveAll(directory); e != nil { 21 | logger.Errorf("Failed to clean up extracted directory: %v", e) 22 | } 23 | } 24 | 25 | if _, e := os.Stat(assets); e == nil { 26 | defer func() { 27 | if e := os.RemoveAll(assets); e != nil { 28 | logger.Errorf("Failed to clean up temporary assets directory: %v", e) 29 | } 30 | }() 31 | } 32 | 33 | logger.Info("Cleaned up.") 34 | 35 | os.Exit(0) 36 | } 37 | 38 | func exists(path string) (bool, error) { 39 | _, err := os.Stat(path) 40 | 41 | if err == nil { 42 | return true, nil 43 | } 44 | 45 | if os.IsNotExist(err) { 46 | return false, nil 47 | } 48 | 49 | return false, err 50 | } 51 | 52 | func filter[T any](ss []T, test func(T) bool) (ret []T) { 53 | for _, s := range ss { 54 | if test(s) { 55 | ret = append(ret, s) 56 | } 57 | } 58 | 59 | return 60 | } 61 | 62 | func loadInfo() { 63 | if info != nil { 64 | return 65 | } 66 | 67 | path := filepath.Join(directory, "Payload", "Discord.app", "Info.plist") 68 | file, err := os.Open(path) 69 | 70 | if err != nil { 71 | logger.Error("Couldn't find Info.plist. Is the provided zip an IPA file?") 72 | exit() 73 | } 74 | 75 | decoder := plist.NewDecoder(file) 76 | if err := decoder.Decode(&info); err != nil { 77 | logger.Error("Couldn't find Info.plist. Is the provided zip an IPA file?") 78 | exit() 79 | } 80 | } 81 | 82 | func saveInfo() { 83 | path := filepath.Join(directory, "Payload", "Discord.app", "Info.plist") 84 | file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0600) 85 | 86 | if err != nil { 87 | logger.Errorf("Failed to open Info.plist for saving: %v", err) 88 | exit() 89 | } 90 | 91 | logger.Debug("Saving Info.plist data...") 92 | encoder := plist.NewEncoder(file) 93 | err = encoder.Encode(info) 94 | 95 | if err != nil { 96 | logger.Errorf("Failed to save Info.plist. %v", err) 97 | exit() 98 | } 99 | 100 | logger.Infof("Saved Info.plist data.") 101 | } 102 | 103 | func download(url string, path string) { 104 | out, err := os.Create(path) 105 | 106 | if err != nil { 107 | logger.Errorf("Failed to pre-write file at %s.", path) 108 | exit() 109 | } 110 | 111 | res, err := http.Get(url) 112 | 113 | if err != nil { 114 | logger.Errorf("Failed to download %s to %s %v", url, path, err) 115 | exit() 116 | } 117 | 118 | defer res.Body.Close() 119 | defer out.Close() 120 | 121 | if res.StatusCode != http.StatusOK { 122 | logger.Errorf("Received bad status while downloading %s: %s", url, res.Status) 123 | exit() 124 | } 125 | 126 | _, err = io.Copy(out, res.Body) 127 | 128 | if err == nil { 129 | logger.Infof("Successfully downloaded \"%s\" to \"%s\".", url, path) 130 | } else { 131 | logger.Errorf("Failed to write \"%s\" to \"%s\": %v.", url, path, err) 132 | exit() 133 | } 134 | } 135 | --------------------------------------------------------------------------------