├── .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 |

18 |

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 |
--------------------------------------------------------------------------------