Skip to content

Commit aeb9d8c

Browse files
committed
add[patcher]: add diff functionality to the patcher
1 parent ebe1e5f commit aeb9d8c

File tree

11 files changed

+428
-20
lines changed

11 files changed

+428
-20
lines changed

.github/workflows/diff.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Diff Checker
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *'
6+
7+
# Check if any previous run
8+
9+
jobs:
10+
diff:
11+
runs-on: windows-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- name: Setup Python 2.7
17+
uses: LizardByte/setup-python-action@master
18+
with:
19+
python-version: '2.7'
20+
- run: pip install -r requirements.txt
21+
- name: Setup IMVU Version
22+
run: |
23+
$ENV:IMVU_VERSION = $(cat VERSION)
24+
echo ("IMVU_VERSION=" + $ENV:IMVU_VERSION) >> $ENV:GITHUB_ENV
25+
- name: Echo Versions
26+
run: |
27+
echo "IMVU_VERSION=${{ env.IMVU_VERSION }}"
28+
- run: python -m t5de --diff

.github/workflows/t5de.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555

5656
- name: Patch Installer
5757
run: |
58-
python -m t5de
58+
python -m t5de --patch
5959
6060
- name: Install NSIS
6161
run: |

scripts/version.py

+27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import os
2+
import sys
3+
import argparse
4+
25
import requests
36

47
from bs4 import BeautifulSoup
@@ -7,6 +10,28 @@
710
VERSION_URL = 'https://www.imvu.com/download.php'
811

912
if __name__ == '__main__':
13+
parser = argparse.ArgumentParser(description='Check for updates')
14+
parser.add_argument('--latest', action='store_true', help='Print the latest version')
15+
parser.add_argument('--current', action='store_true', help='Print the current version')
16+
parser.add_argument('--update', action='store_true', help='Check for updates')
17+
18+
args = parser.parse_args()
19+
20+
if args.latest:
21+
response = requests.get(VERSION_URL)
22+
soup = BeautifulSoup(response.content, 'html.parser')
23+
h2 = soup.find('h2', text='IMVU Classic:')
24+
b = h2.find_next('b')
25+
latest_version = b.text.strip(':')
26+
print(latest_version)
27+
sys.exit(0)
28+
29+
if args.current:
30+
with open(VERSION_PATH, 'r') as f:
31+
version = f.read().strip()
32+
print(version)
33+
sys.exit(0)
34+
1035
cwd = os.getcwd()
1136

1237
print('READING VERSION: {}'.format(VERSION_PATH))
@@ -25,5 +50,7 @@
2550

2651
if version != latest_version:
2752
print('UPDATE AVAILABLE')
53+
sys.exit(1)
2854
else:
2955
print('NO UPDATE AVAILABLE')
56+
sys.exit(0)

t5de/Client.py

+127-10
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@
44

55
import autogui
66
import requests
7+
from bs4 import BeautifulSoup
78

89
from typing import List
910

1011

12+
# noinspection PyBroadException
1113
class Client:
14+
"""
15+
A class to manage the IMVU client
16+
17+
Attributes:
18+
cwd (str): The current working directory
19+
version (str): The version of IMVU to manipulate
20+
"""
21+
1222
BASE_URL = 'https://static-akm.imvu.com/imvufiles/installers/InstallIMVU_{}.exe'
23+
VERSION_URL = 'https://www.imvu.com/download.php'
1324
CLIENT_PATH = '{}/IMVUClient'.format(os.getenv('APPDATA'))
1425

15-
def __init__(self, cwd, version):
16-
self.cwd = cwd
17-
self.version = version
26+
def __init__(self, version=None, cwd=None):
27+
"""
28+
:param str|None version: The version of IMVU to manipulate, if None, the latest version is used
29+
:param str|None cwd: The current working directory
30+
"""
31+
self.cwd = cwd or os.getcwd()
32+
self.version = version or self._latest_version()
1833

1934
def __enter__(self):
2035
return self
@@ -23,27 +38,58 @@ def __exit__(self, exc_type, exc_val, exc_tb):
2338
self.cleanup()
2439

2540
def download(self):
41+
"""
42+
Download the IMVU client executable if it does not exist
43+
44+
:return: True if the executable was downloaded, False otherwise
45+
:rtype: bool
46+
"""
2647
print('DOWNLOADING: IMVU {}'.format(self.version))
2748
if os.path.isfile(os.path.join(self.cwd, 'InstallIMVU_%s.exe' % self.version)):
28-
return
49+
return False
2950

3051
r = requests.get(Client.BASE_URL.format(self.version))
3152

3253
with open(os.path.join(self.cwd, 'InstallIMVU_%s.exe' % self.version), 'wb') as f:
3354
f.write(r.content)
3455

35-
def install(self):
56+
return True
57+
58+
def install(self, download=False):
59+
"""
60+
Install the IMVU client using the downloaded executable
61+
:param bool download: Download the executable if it does not exist
62+
"""
63+
if not os.path.isfile(os.path.join(self.cwd, 'InstallIMVU_%s.exe' % self.version)):
64+
if download:
65+
self.download()
66+
else:
67+
raise IOError('Could not find InstallIMVU_{}.exe'.format(self.version))
68+
3669
print('INSTALLING: IMVU {}'.format(self.version))
3770
autogui.open(os.path.join(self.cwd, 'InstallIMVU_%s.exe' % self.version), setActive=False)
38-
autogui.setWindow('IMVU Setup')
39-
autogui.click('Install', 0, 4)
71+
attempts = 0
72+
while attempts < 5:
73+
attempts += 1
74+
try:
75+
autogui.setWindow('IMVU Setup')
76+
autogui.click('Install', 0, 4)
77+
break
78+
except Exception:
79+
if attempts > 5:
80+
raise RuntimeError('Could not automate IMVU install! Failed to find Setup window.')
81+
print('SLEEPING: 5 SECONDS...')
82+
time.sleep(5)
4083
print('SLEEPING: 15 SECONDS...')
4184
time.sleep(15)
4285

4386
attempts = 0
4487
while attempts < 5:
4588
attempts += 1
4689
try:
90+
if autogui.exists('Update Available'):
91+
autogui.setWindow('Update Available')
92+
autogui.click('Close', 0, 4)
4793
autogui.setWindow('IMVU Login')
4894
autogui.click('Close', 0, 4)
4995
break
@@ -68,10 +114,13 @@ def copy(self):
68114

69115
def patch(self, patchers, dry_run=False):
70116
"""
71-
:param List patchers:
72-
:param dry_run:
73-
:return:
117+
Patch the IMVU client
118+
:param List patchers: A list of patchers to run
119+
:param bool dry_run: Do not apply patches, only simulate
74120
"""
121+
if not os.path.isdir(os.path.join(self.cwd, 'IMVUClient')):
122+
raise IOError('Could not find IMVUClient directory')
123+
75124
for Patcher in patchers:
76125
patcher = Patcher(self.cwd)
77126
patcher.setup()
@@ -83,3 +132,71 @@ def cleanup(self):
83132
executable = os.path.join(self.cwd, 'InstallIMVU_%s.exe' % self.version)
84133
if os.path.isfile(executable):
85134
os.remove(executable)
135+
136+
def diff(self, generators, version=None):
137+
"""
138+
:param List[constructor] generators: A list of diff generators
139+
:param str version: The version to diff against, if None, the latest version is used
140+
"""
141+
if version is None:
142+
version = self._latest_version()
143+
144+
if self.version == version:
145+
print('SKIPPED: IMVU {} and IMVU {} are the same'.format(self.version, version))
146+
return
147+
148+
if os.path.isdir(os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version))):
149+
shutil.rmtree(os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version)))
150+
151+
print('DIFFING: IMVU {} and IMVU {}'.format(self.version, version))
152+
153+
# Download the previous version first
154+
self.download()
155+
self.install()
156+
self.copy()
157+
158+
# Rename the previous version to IMVUClient-{version}
159+
if os.path.isdir(os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version))):
160+
shutil.rmtree(os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version)))
161+
162+
shutil.move(os.path.join(self.cwd, 'IMVUClient'), os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version)))
163+
164+
# Download the current version
165+
current_version = self.version
166+
self.version = version
167+
168+
self.download()
169+
self.install()
170+
self.copy()
171+
172+
# Rename the current version to IMVUClient-{current}
173+
if os.path.isdir(os.path.join(self.cwd, 'IMVUClient-{}'.format(version))):
174+
shutil.rmtree(os.path.join(self.cwd, 'IMVUClient-{}'.format(version)))
175+
176+
shutil.move(os.path.join(self.cwd, 'IMVUClient'), os.path.join(self.cwd, 'IMVUClient-{}'.format(version)))
177+
178+
self.version = current_version
179+
180+
previous_cwd = os.path.join(self.cwd, 'IMVUClient-{}'.format(self.version))
181+
current_cwd = os.path.join(self.cwd, 'IMVUClient-{}'.format(version))
182+
183+
for Generator in generators:
184+
print('PROCESSING DIFF GENERATOR: {}'.format(Generator.__name__))
185+
with Generator(self.cwd, self.version, version, previous_cwd, current_cwd) as generator:
186+
generator.diff()
187+
188+
executable = os.path.join(self.cwd, 'InstallIMVU_%s.exe' % version)
189+
if os.path.isfile(executable):
190+
os.remove(executable)
191+
192+
def _latest_version(self):
193+
"""
194+
Get the latest version of IMVU
195+
:return: The latest version of IMVU
196+
:rtype: str
197+
"""
198+
response = requests.get(Client.VERSION_URL)
199+
soup = BeautifulSoup(response.content, 'html.parser')
200+
h2 = soup.find('h2', text='IMVU Classic:')
201+
b = h2.find_next('b')
202+
return b.text.strip(':')

t5de/__main__.py

+35-9
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,50 @@
33
import os
44
import sys
55
import dotenv
6+
import argparse
67

78
from .patchers import InterfacePatcher, PythonPatcher, ChecksumPatcher
9+
from .diff import InterfaceDiff, PythonDiff
810
from . import Client
911

1012

1113
def main():
12-
IMVU_VERSION = os.getenv('IMVU_VERSION')
13-
T5DE_VERSION = os.getenv('T5DE_VERSION')
14+
imvu_version = os.getenv('IMVU_VERSION')
15+
t5de_version = os.getenv('T5DE_VERSION')
1416

15-
print('Running T5DE patcher v%s for IMVU v%s' % (T5DE_VERSION, IMVU_VERSION))
17+
if not imvu_version:
18+
print('IMVU_VERSION environment variable not set')
19+
return 1
1620

17-
with Client(os.getcwd(), IMVU_VERSION) as patcher:
18-
patcher.download()
19-
patcher.install()
20-
patcher.copy()
21-
patcher.patch(patchers=[InterfacePatcher, PythonPatcher, ChecksumPatcher], dry_run=False)
21+
if not t5de_version:
22+
print('T5DE_VERSION environment variable not set')
23+
return 1
24+
25+
print('Running T5DE patcher v%s for IMVU v%s' % (t5de_version, imvu_version))
26+
27+
parser = argparse.ArgumentParser(prog='t5de', description='The T5DE patcher for IMVU')
28+
parser.add_argument('--dry-run', action='store_true', help='Perform a dry run')
29+
parser.add_argument('--diff', help='Generate a diff between two IMVU versions', const='latest', nargs='?', type=str)
30+
parser.add_argument('--version', help='The current IMVU version')
31+
parser.add_argument('--patch', action='store_true', help='Patch the current IMVU version')
32+
33+
args = parser.parse_args()
34+
35+
if args.diff:
36+
version = None if args.diff == 'latest' else args.diff
37+
with Client(args.version or imvu_version) as client:
38+
client.diff(generators=[InterfaceDiff, PythonDiff], version=version)
39+
40+
if args.patch:
41+
with Client(args.version or imvu_version) as patcher:
42+
patcher.download()
43+
patcher.install()
44+
patcher.copy()
45+
patcher.patch(patchers=[InterfacePatcher, PythonPatcher, ChecksumPatcher], dry_run=False)
46+
47+
return 0
2248

2349

2450
if __name__ == '__main__':
25-
dotenv.load_dotenv()
51+
dotenv.load_dotenv(override=True)
2652
sys.exit(main())

t5de/diff/Diff.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
4+
class Diff(object):
5+
__metaclass__ = ABCMeta
6+
"""
7+
Base class for diff generators
8+
9+
Attributes:
10+
cwd (str): The current working directory
11+
previous (str): The previous version
12+
current (str): The current version
13+
previous_cwd (str): The previous version's working directory
14+
current_cwd (str): The current version's working directory
15+
"""
16+
17+
def __init__(self, cwd, previous, current, previous_cwd, current_cwd):
18+
"""
19+
:param str cwd:
20+
:param str previous:
21+
:param str current:
22+
:param str previous_cwd:
23+
:param str current_cwd:
24+
"""
25+
self.cwd = cwd
26+
self.previous = previous
27+
self.current = current
28+
self.previous_cwd = previous_cwd
29+
self.current_cwd = current_cwd
30+
31+
@abstractmethod
32+
def setup(self):
33+
raise NotImplementedError
34+
35+
@abstractmethod
36+
def diff(self):
37+
"""
38+
Generate a diff between the previous and current versions
39+
"""
40+
raise NotImplementedError
41+
42+
@abstractmethod
43+
def cleanup(self):
44+
raise NotImplementedError
45+
46+
def __enter__(self):
47+
self.setup()
48+
return self
49+
50+
def __exit__(self, exc_type, exc_value, traceback):
51+
self.cleanup()
52+
53+
def _diff(self):
54+
self.diff()

0 commit comments

Comments
 (0)