Repository: mindstorm38/portablemc (language: python)

Issue:
Make a specific auth session for offline user
Make a specific auth session variant for offline users to avoid `username` and `uuid` in `StartOptions`


Discussion / hints:

Fix patch:
diff --git a/src/core/portablemc/__init__.py b/src/core/portablemc/__init__.py
index 16e4a7fb..d1b608ae 100644
--- a/src/core/portablemc/__init__.py
+++ b/src/core/portablemc/__init__.py
@@ -42,7 +42,7 @@
 __all__ = [
     "LAUNCHER_NAME", "LAUNCHER_VERSION", "LAUNCHER_AUTHORS", "LAUNCHER_COPYRIGHT", "LAUNCHER_URL",
     "Context", "Version", "StartOptions", "Start", "VersionManifest",
-    "AuthSession", "YggdrasilAuthSession", "MicrosoftAuthSession", "AuthDatabase",
+    "AuthSession", "OfflineAuthSession", "YggdrasilAuthSession", "MicrosoftAuthSession", "AuthDatabase",
     "DownloadEntry", "DownloadList", "DownloadProgress", "DownloadEntryProgress",
     "BaseError", "JsonRequestError", "AuthError", "VersionManifestError", "VersionError", "JvmLoadingError",
         "DownloadReport",
@@ -560,8 +560,8 @@ class StartOptions:
 
     def __init__(self):
         self.auth_session: Optional[AuthSession] = None
-        self.uuid: Optional[str] = None
-        self.username: Optional[str] = None
+        self.uuid: Optional[str] = None      # DEPRECATED, use OfflineAuthSession or 'with_offline'
+        self.username: Optional[str] = None  # DEPRECATED, use OfflineAuthSession or 'with_offline'
         self.demo: bool = False
         self.resolution: Optional[Tuple[int, int]] = None
         self.disable_multiplayer: bool = False
@@ -581,8 +581,7 @@ def with_online(cls, auth_session: 'AuthSession') -> 'StartOptions':
     @classmethod
     def with_offline(cls, username: Optional[str], uuid: Optional[str]) -> 'StartOptions':
         opts = StartOptions()
-        opts.username = username
-        opts.uuid = uuid
+        opts.auth_session = OfflineAuthSession(uuid, username)
         return opts
 
 
@@ -647,22 +646,10 @@ def prepare(self, opts: StartOptions):
             **opts.features
         }
 
-        # Auth
-        if opts.auth_session is not None:
-            uuid = opts.auth_session.uuid
-            username = opts.auth_session.username
-        else:
-            namespace_hash = UUID("8df5a464-38de-11ec-aa66-3fd636ee2ed7")
-            uuid_valid = opts.uuid is not None and len(opts.uuid) == 32
-            if uuid_valid:
-                uuid = opts.uuid
-                username = uuid[:8] if opts.username is None else opts.username[:16]
-            elif opts.username is None:
-                uuid = uuid5(namespace_hash, platform.node()).hex
-                username = uuid[:8]
-            else:
-                username = opts.username[:16]
-                uuid = uuid5(namespace_hash, username).hex
+        # Auth (we fallback to OfflineAuthSession and deprecated 'opts.username' and 'opts.uuid' if not set)
+        auth_session = OfflineAuthSession(opts.username, opts.uuid) if opts.auth_session is None else opts.auth_session
+        uuid = auth_session.uuid
+        username = auth_session.username
 
         # Arguments replacements
         self.args_replacements = {
@@ -674,13 +661,13 @@ def prepare(self, opts: StartOptions):
             "assets_root": self.version.context.assets_dir,
             "assets_index_name": self.version.assets_index_version,
             "auth_uuid": uuid,
-            "auth_access_token": "" if opts.auth_session is None else opts.auth_session.format_token_argument(False),
-            "auth_xuid": "" if opts.auth_session is None else opts.auth_session.get_xuid(),
-            "clientid": "" if opts.auth_session is None else opts.auth_session.client_id,
-            "user_type": "" if opts.auth_session is None else opts.auth_session.user_type,
+            "auth_access_token": auth_session.format_token_argument(False),
+            "auth_xuid": auth_session.get_xuid(),
+            "clientid": auth_session.client_id,
+            "user_type": auth_session.user_type,
             "version_type": self.version.version_meta.get("type", ""),
             # Game (legacy)
-            "auth_session": "" if opts.auth_session is None else opts.auth_session.format_token_argument(True),
+            "auth_session": auth_session.format_token_argument(True),
             "game_assets": self.version.assets_virtual_dir,
             "user_properties": "{}",
             # JVM
@@ -911,6 +898,30 @@ def invalidate(self):
         pass
 
 
+class OfflineAuthSession(AuthSession):
+
+    type = "offline"
+    user_type = ""
+
+    def __init__(self, username: Optional[str], uuid: Optional[str]):
+        super().__init__()
+        if uuid is not None and len(uuid) == 32:
+            # If the UUID is already valid.
+            self.uuid = uuid
+            self.username = uuid[:8] if username is None else username[:16]
+        else:
+            namespace_hash = UUID("8df5a464-38de-11ec-aa66-3fd636ee2ed7")
+            if username is None:
+                self.uuid = uuid5(namespace_hash, platform.node()).hex
+                self.username = self.uuid[:8]
+            else:
+                self.username = username[:16]
+                self.uuid = uuid5(namespace_hash, self.username).hex
+
+    def format_token_argument(self, legacy: bool) -> str:
+        return ""
+
+
 class YggdrasilAuthSession(AuthSession):
 
     type = "yggdrasil"
diff --git a/src/core/portablemc/cli.py b/src/core/portablemc/cli.py
index 793a0102..2e7a7fd9 100644
--- a/src/core/portablemc/cli.py
+++ b/src/core/portablemc/cli.py
@@ -468,8 +468,7 @@ def _pretty_logger_finalize():
         else:
             if ns.microsoft:
                 print_task("WARN", "auth.microsoft_requires_email", done=True)
-            start_opts.uuid = ns.uuid
-            start_opts.username = ns.username
+            start_opts.auth_session = OfflineAuthSession(ns.username, ns.uuid)
 
         print_task("", "start.starting")
 


Test patch:
diff --git a/test/core/test_install.py b/test/core/test_install.py
index 8f815620..009eabbe 100644
--- a/test/core/test_install.py
+++ b/test/core/test_install.py
@@ -38,3 +38,19 @@ def test_install(tmp_path, version):
 
     start = Start(ver)
     start.prepare(StartOptions())
+
+    repl = start.args_replacements
+
+    assert repl["version_name"] == version
+    assert repl["library_directory"] == str(tmp_path / "libraries")
+    assert repl["game_directory"] == str(tmp_path)
+    assert repl["assets_root"] == str(tmp_path / "assets")
+
+    # Because we are in offline mode:
+    assert not len(repl["auth_access_token"])
+    assert not len(repl["auth_xuid"])
+    assert not len(repl["clientid"])
+    assert not len(repl["user_type"])
+    assert not len(repl["auth_session"])
+
+    assert repl["launcher_name"] == "portablemc"