mcserver-cli 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. mcserver/__init__.py +1 -0
  2. mcserver/__main__.py +3 -0
  3. mcserver/app.py +24 -0
  4. mcserver/caching.py +0 -0
  5. mcserver/cli.py +326 -0
  6. mcserver/commands/__init__.py +39 -0
  7. mcserver/commands/add_projects.py +7 -0
  8. mcserver/commands/import_setup.py +6 -0
  9. mcserver/commands/initialize_server.py +119 -0
  10. mcserver/commands/list_projects.py +6 -0
  11. mcserver/commands/op_grant.py +7 -0
  12. mcserver/commands/op_list.py +32 -0
  13. mcserver/commands/op_revoke.py +7 -0
  14. mcserver/commands/print_help.py +7 -0
  15. mcserver/commands/remove_projects.py +6 -0
  16. mcserver/commands/search_projects.py +7 -0
  17. mcserver/commands/server_status.py +6 -0
  18. mcserver/commands/show_projects.py +26 -0
  19. mcserver/commands/start_server.py +150 -0
  20. mcserver/commands/stop_server.py +46 -0
  21. mcserver/commands/update_projects.py +6 -0
  22. mcserver/commands/whitelist_add.py +7 -0
  23. mcserver/commands/whitelist_list.py +31 -0
  24. mcserver/commands/whitelist_remove.py +31 -0
  25. mcserver/commandsa.py +1297 -0
  26. mcserver/config.py +37 -0
  27. mcserver/constants.py +14 -0
  28. mcserver/fabricmc/meta.py +41 -0
  29. mcserver/legacyfabric/meta.py +37 -0
  30. mcserver/metadata.py +33 -0
  31. mcserver/minecraft/api.py +94 -0
  32. mcserver/minecraft/player_identity.py +21 -0
  33. mcserver/modrinth/api.py +657 -0
  34. mcserver/modrinth/modpack.py +101 -0
  35. mcserver/modrinth/project_records/metadata.py +0 -0
  36. mcserver/modrinth/project_records/relationships.py +203 -0
  37. mcserver/modrinth/project_records/shared.py +9 -0
  38. mcserver/modrinth/projects.py +80 -0
  39. mcserver/modrinth/shared.py +13 -0
  40. mcserver/modrinth/slug_index.py +36 -0
  41. mcserver/mojang/eula.py +28 -0
  42. mcserver/mojang/manifest.py +69 -0
  43. mcserver/mysterious.py +5 -0
  44. mcserver/networking.py +152 -0
  45. mcserver/papermc/api.py +81 -0
  46. mcserver/purpurmc/api.py +81 -0
  47. mcserver/quiltmc/meta.py +54 -0
  48. mcserver/resolver.py +164 -0
  49. mcserver/server_files/banned_ips.py +120 -0
  50. mcserver/server_files/banned_players.py +166 -0
  51. mcserver/server_files/ops.py +143 -0
  52. mcserver/server_files/properties.py +41 -0
  53. mcserver/server_files/usercaches.py +0 -0
  54. mcserver/server_files/whitelist.py +163 -0
  55. mcserver/shared.py +198 -0
  56. mcserver/state.py +77 -0
  57. mcserver_cli-3.0.0.dist-info/METADATA +63 -0
  58. mcserver_cli-3.0.0.dist-info/RECORD +61 -0
  59. mcserver_cli-3.0.0.dist-info/WHEEL +4 -0
  60. mcserver_cli-3.0.0.dist-info/entry_points.txt +2 -0
  61. mcserver_cli-3.0.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,166 @@
1
+ # Modules
2
+ # Standard
3
+ import json
4
+ import logging as log
5
+ from pathlib import Path
6
+ from typing import Literal, TypedDict
7
+ from uuid import UUID
8
+
9
+ # MCServer
10
+ from mcserver.minecraft.player_identity import (
11
+ PlayerID,
12
+ PlayerUUID,
13
+ normalize_player_uuid,
14
+ )
15
+
16
+ # Paths
17
+ banned_players_file = Path("banned-players.json")
18
+
19
+
20
+ # TypedDicts
21
+ class BannedPlayerEntry(TypedDict):
22
+ uuid: str
23
+ name: str
24
+ created: str # Time Format: yyyy-MM-dd HH:mm:ss Z
25
+ source: str
26
+ expires: str | Literal["forever"] # Time format: yyyy-MM-dd HH:mm:ss Z
27
+ reason: str
28
+
29
+
30
+ class BannedPlayer(TypedDict):
31
+ player_uuid: PlayerUUID
32
+ player_name: str
33
+ created_time: str # Time format: yyyy-MM-dd HH:mm:ss Z
34
+ ban_source: str
35
+ expires_time: str | Literal["forever"] # Time format: yyyy-MM-dd HH:mm:ss Z
36
+ ban_reason: str
37
+
38
+
39
+ # Functions
40
+ # Add Players
41
+ def add_player(
42
+ player_uuid: PlayerUUID,
43
+ player_name: str,
44
+ created_time: str,
45
+ ban_source: str,
46
+ expires_time: str | Literal["forever"] = "forever",
47
+ ban_reason: str = "Banned via MCServer.",
48
+ ) -> None:
49
+ player_uuid = normalize_player_uuid(player_uuid)
50
+ banned_player: BannedPlayer = {
51
+ "player_uuid": player_uuid,
52
+ "player_name": player_name,
53
+ "created_time": created_time,
54
+ "ban_source": ban_source,
55
+ "expires_time": expires_time,
56
+ "ban_reason": ban_reason,
57
+ }
58
+
59
+ add_players(banned_player)
60
+
61
+
62
+ def add_players(banned_players: list[BannedPlayer] | BannedPlayer) -> None:
63
+ if not isinstance(banned_players, list):
64
+ banned_players = [banned_players]
65
+
66
+ with open(banned_players_file, mode="r+") as file:
67
+ banned_players_data = json.load(file)
68
+ for player in banned_players:
69
+ player_uuid = str(normalize_player_uuid(player["player_uuid"]))
70
+ player_name = player["player_name"]
71
+ ban_reason = player["ban_reason"]
72
+
73
+ banned_player_entry: BannedPlayerEntry = {
74
+ "uuid": player_uuid,
75
+ "name": player_name,
76
+ "created": player["created_time"],
77
+ "source": player["ban_source"],
78
+ "expires": player["expires_time"],
79
+ "reason": ban_reason,
80
+ }
81
+
82
+ banned_players_data.append(banned_player_entry)
83
+ log.debug(
84
+ f"Added player '{player_name}' ({player_uuid}) to the banned player list with reason '{ban_reason}'"
85
+ )
86
+
87
+ file.seek(0)
88
+ json.dump(banned_players_data, file, indent=2)
89
+ file.truncate()
90
+
91
+
92
+ # Remove Players
93
+ def remove_player(
94
+ player_uuid: PlayerUUID | None = None, player_name: str | None = None
95
+ ) -> None:
96
+ if player_uuid is None and player_name is None:
97
+ raise ValueError(
98
+ "At least one of 'player_uuid' or 'player_name' must be provided"
99
+ )
100
+
101
+ player_id: PlayerID = {}
102
+ if player_uuid is not None:
103
+ player_uuid = normalize_player_uuid(player_uuid)
104
+ player_id["player_uuid"] = normalize_player_uuid(player_uuid)
105
+
106
+ if player_name is not None:
107
+ player_id["player_name"] = player_name
108
+
109
+ remove_players(player_id)
110
+
111
+
112
+ def remove_players(player_ids: list[PlayerID] | PlayerID) -> None:
113
+ if not isinstance(player_ids, list):
114
+ player_ids = [player_ids]
115
+
116
+ player_uuids: set[PlayerUUID | None] = set()
117
+ player_names: set[str | None] = set()
118
+ for player_id in player_ids:
119
+ if "player_uuid" in player_id:
120
+ player_uuids.add(player_id["player_uuid"])
121
+
122
+ if "player_name" in player_id:
123
+ player_names.add(player_id["player_name"])
124
+
125
+ with open(banned_players_file, mode="r+") as file:
126
+ banned_players_data: list[BannedPlayerEntry] = json.load(file)
127
+ new_banned_players_data: list[BannedPlayerEntry] = []
128
+ for player in banned_players_data:
129
+ player_uuid = player["uuid"]
130
+ player_name = player["name"]
131
+
132
+ if player_uuid in player_uuids or player_name in player_names:
133
+ log.debug(
134
+ f"Removed player '{player_name}' ({player_uuid}) from the banned player list"
135
+ )
136
+ continue
137
+
138
+ new_banned_players_data.append(player)
139
+
140
+ file.seek(0)
141
+ json.dump(new_banned_players_data, file, indent=2)
142
+ file.truncate()
143
+
144
+
145
+ # TODO: add 'update_player' and 'update_players', just use plain dict.update() because it has no depth
146
+
147
+
148
+ # List Players
149
+ def list_players() -> list[BannedPlayer]:
150
+ banned_players_data: list[BannedPlayerEntry] = json.loads(
151
+ banned_players_file.read_text()
152
+ )
153
+ banned_players: list[BannedPlayer] = []
154
+ for entry in banned_players_data:
155
+ banned_player: BannedPlayer = {
156
+ "player_uuid": UUID(entry["uuid"]),
157
+ "player_name": entry["name"],
158
+ "created_time": entry["created"],
159
+ "ban_source": entry["source"],
160
+ "expires_time": entry["expires"],
161
+ "ban_reason": entry["reason"],
162
+ }
163
+
164
+ banned_players.append(banned_player)
165
+
166
+ return banned_players
@@ -0,0 +1,143 @@
1
+ # Modules
2
+ # Standard
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Literal, TypedDict
6
+ from uuid import UUID
7
+
8
+ # MCServer
9
+ from mcserver.minecraft.player_identity import (
10
+ PlayerID,
11
+ PlayerUUID,
12
+ normalize_player_uuid,
13
+ )
14
+
15
+ # Paths
16
+ ops_file = Path("ops.json")
17
+
18
+
19
+ # TypedDicts
20
+ class OperatorEntry(TypedDict):
21
+ uuid: str
22
+ name: str
23
+ level: Literal[0, 1, 2, 3, 4]
24
+ bypassesPlayerLimit: bool
25
+
26
+
27
+ class OperatorPlayer(TypedDict):
28
+ player_uuid: PlayerUUID
29
+ player_name: str
30
+ permission_level: Literal[0, 1, 2, 3, 4]
31
+ bypasses_player_limit: bool
32
+
33
+
34
+ # Functions
35
+ # Add Players
36
+ def add_player(
37
+ player_uuid: PlayerUUID,
38
+ player_name: str,
39
+ permission_level: Literal[0, 1, 2, 3, 4],
40
+ bypasses_player_limit: bool = False,
41
+ ) -> None:
42
+ operator_player: OperatorPlayer = {
43
+ "player_uuid": player_uuid,
44
+ "player_name": player_name,
45
+ "permission_level": permission_level,
46
+ "bypasses_player_limit": bypasses_player_limit,
47
+ }
48
+
49
+ add_players(operator_player)
50
+
51
+
52
+ def add_players(operator_players: list[OperatorPlayer] | OperatorPlayer) -> None:
53
+ if not isinstance(operator_players, list):
54
+ operator_players = [operator_players]
55
+
56
+ with open(ops_file, mode="r+") as file:
57
+ ops_data: list[OperatorEntry] = json.load(file)
58
+
59
+ for player in operator_players:
60
+ permission_level = player["permission_level"]
61
+ if permission_level < 0 or permission_level > 4:
62
+ raise ValueError("Permission level must be between 0 and 4.")
63
+
64
+ operator_entry: OperatorEntry = {
65
+ "uuid": str(normalize_player_uuid(player["player_uuid"])),
66
+ "name": player["player_name"],
67
+ "level": permission_level,
68
+ "bypassesPlayerLimit": player["bypasses_player_limit"],
69
+ }
70
+
71
+ ops_data.append(operator_entry)
72
+
73
+ file.seek(0)
74
+ json.dump(ops_data, file, indent=2)
75
+ file.truncate()
76
+
77
+
78
+ # Remove Players
79
+ def remove_player(
80
+ player_uuid: PlayerUUID | None = None, player_name: str | None = None
81
+ ) -> None:
82
+ if player_uuid is None and player_name is None:
83
+ raise ValueError(
84
+ "At least one of 'player_uuid' or 'player_name' must be provided"
85
+ )
86
+
87
+ player_id: PlayerID = {}
88
+ if player_uuid is not None:
89
+ player_id["player_uuid"] = normalize_player_uuid(player_uuid)
90
+
91
+ if player_name is not None:
92
+ player_id["player_name"] = player_name
93
+
94
+ remove_players(player_id)
95
+
96
+
97
+ def remove_players(player_ids: list[PlayerID] | PlayerID) -> None:
98
+ if not isinstance(player_ids, list):
99
+ player_ids = [player_ids]
100
+
101
+ player_uuids: set[PlayerUUID | None] = set()
102
+ player_names: set[str | None] = set()
103
+
104
+ for player_id in player_ids:
105
+ if "player_uuid" in player_id:
106
+ player_uuids.add(player_id["player_uuid"])
107
+
108
+ if "player_name" in player_id:
109
+ player_names.add(player_id["player_name"])
110
+
111
+ with open(ops_file, mode="r+") as file:
112
+ ops_data: list[OperatorEntry] = json.load(file)
113
+
114
+ new_ops_data: list[OperatorEntry] = []
115
+ for player in ops_data:
116
+ if player["uuid"] in player_uuids or player["name"] in player_names:
117
+ continue
118
+
119
+ new_ops_data.append(player)
120
+
121
+ file.seek(0)
122
+ json.dump(new_ops_data, file, indent=2)
123
+ file.truncate()
124
+
125
+
126
+ # TODO: add 'update_player' and 'update_players', just use plain dict.update() because it has no depth
127
+
128
+
129
+ # List Players
130
+ def list_players() -> list[OperatorPlayer]:
131
+ ops_data: list[OperatorEntry] = json.loads(ops_file.read_text())
132
+ operator_players: list[OperatorPlayer] = []
133
+ for entry in ops_data:
134
+ operator_player: OperatorPlayer = {
135
+ "player_uuid": UUID(entry["uuid"]),
136
+ "player_name": entry["name"],
137
+ "permission_level": entry["level"],
138
+ "bypasses_player_limit": entry["bypassesPlayerLimit"],
139
+ }
140
+
141
+ operator_players.append(operator_player)
142
+
143
+ return operator_players
@@ -0,0 +1,41 @@
1
+ # Imports
2
+ from typing import TypedDict
3
+
4
+
5
+ # TypedDicts
6
+ class ParsedProperties(TypedDict):
7
+ comments: list[str]
8
+ data: dict[str, str]
9
+
10
+
11
+ # Functions
12
+ def loads(properties_data: str) -> ParsedProperties:
13
+ parsed_properties: ParsedProperties = {"comments": [], "data": {}}
14
+
15
+ for line in properties_data.splitlines():
16
+ line = line.strip()
17
+ if not line:
18
+ continue
19
+
20
+ if line.startswith("#"):
21
+ parsed_properties["comments"].append(line)
22
+
23
+ if not "=" in line:
24
+ continue
25
+
26
+ key, value = line.split("=", 1)
27
+ parsed_properties[key.strip()] = value
28
+
29
+ return parsed_properties
30
+
31
+
32
+ # Dump only support comments that are oon top of the file
33
+ def dumps(parsed_properties: ParsedProperties) -> str:
34
+ lines: list[str] = []
35
+ for comment in parsed_properties["comments"]:
36
+ lines.append(f"#{comment}")
37
+
38
+ for key, value in parsed_properties["data"].items():
39
+ lines.append(f"{key}={value}")
40
+
41
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,163 @@
1
+ # Modules
2
+ # Standard
3
+ import json
4
+ import logging as log
5
+ from pathlib import Path
6
+ from typing import TypedDict
7
+ from uuid import UUID
8
+
9
+ # MCServer
10
+ from mcserver.minecraft.player_identity import (
11
+ PlayerID,
12
+ PlayerUUID,
13
+ normalize_player_uuid,
14
+ )
15
+
16
+ # Paths
17
+ whitelist_file = Path("whitelist.json")
18
+
19
+
20
+ # TypedDicts
21
+ class WhitelistEntry(TypedDict):
22
+ uuid: str
23
+ name: str
24
+
25
+
26
+ class WhitelistPlayer(TypedDict):
27
+ player_uuid: PlayerUUID
28
+ player_name: str
29
+
30
+
31
+ # Functions
32
+ # Add Players
33
+ def add_player(player_uuid: PlayerUUID, player_name: str) -> None:
34
+ whitelist_player: WhitelistPlayer = {
35
+ "player_uuid": player_uuid,
36
+ "player_name": player_name,
37
+ }
38
+
39
+ add_players(whitelist_player)
40
+
41
+
42
+ def add_players(whitelist_players: list[WhitelistPlayer] | WhitelistPlayer) -> None:
43
+ if not isinstance(whitelist_players, list):
44
+ whitelist_players = [whitelist_players]
45
+
46
+ with open(whitelist_file, mode="r+") as file:
47
+ whitelist_data = json.load(file)
48
+
49
+ for player in whitelist_players:
50
+ whitelist_entry: WhitelistEntry = {
51
+ "uuid": str(normalize_player_uuid(player["player_uuid"])),
52
+ "name": player.get("player_name", ""),
53
+ }
54
+
55
+ whitelist_data.append(whitelist_entry)
56
+
57
+ file.seek(0)
58
+ json.dump(whitelist_data, file, indent=2)
59
+ file.truncate()
60
+
61
+
62
+ # Remove Players
63
+ def remove_player(
64
+ player_uuid: PlayerUUID | None = None, player_name: str | None = None
65
+ ) -> None:
66
+ if player_uuid is None and player_name is None:
67
+ raise ValueError(
68
+ "At least one of 'player_uuid' or 'player_name' must be provided"
69
+ )
70
+
71
+ player_id: PlayerID = {}
72
+ if player_uuid is not None:
73
+ player_uuid = normalize_player_uuid(player_uuid)
74
+ player_id["player_uuid"] = normalize_player_uuid(player_uuid)
75
+
76
+ if player_name is not None:
77
+ player_id["player_name"] = player_name
78
+
79
+ remove_players(player_id)
80
+
81
+
82
+ def remove_players(player_ids: list[PlayerID] | PlayerID) -> None:
83
+ """Remove player entries matching the exact UUIDs or names
84
+
85
+ Args:
86
+ player_ids: Player UUIDs and/or names object
87
+
88
+ Returns:
89
+ None: Returns nothing
90
+
91
+ Raises:
92
+ FileNotFoundError: The 'whitelist.json' file not found
93
+ """
94
+ if not isinstance(player_ids, list):
95
+ player_ids = [player_ids]
96
+
97
+ player_uuids: set[PlayerUUID | None] = set()
98
+ player_names: set[str | None] = set()
99
+ for player_id in player_ids:
100
+ if "player_uuid" in player_id:
101
+ player_uuids.add(player_id["player_uuid"])
102
+
103
+ if "player_name" in player_id:
104
+ player_names.add(player_id["player_name"])
105
+
106
+ if not whitelist_file.exists():
107
+ raise FileNotFoundError(f"File '{whitelist_file}' is not found.")
108
+
109
+ with open(whitelist_file, mode="r+") as file:
110
+ whitelist_data: list[WhitelistEntry] = json.load(file)
111
+ new_whitelist_data: list[WhitelistEntry] = []
112
+ for player in whitelist_data:
113
+ player_uuid = player["uuid"]
114
+ player_name = player["name"]
115
+
116
+ if player_uuid in player_uuids or player_name in player_names:
117
+ log.debug(
118
+ f"Removed player '{player_name}' ({player_uuid}) from the whitelisted player list"
119
+ )
120
+ continue
121
+
122
+ new_whitelist_data.append(player)
123
+
124
+ file.seek(0)
125
+ json.dump(new_whitelist_data, file, indent=2)
126
+ file.truncate()
127
+
128
+
129
+ # TODO: add 'update_player' and 'update_players', just use plain dict.update() because it has no depth
130
+
131
+
132
+ # List Players
133
+ def list_players() -> list[WhitelistPlayer]:
134
+ whitelist_data: list[WhitelistEntry] = json.loads(whitelist_file.read_text())
135
+
136
+ whitelist_players: list[WhitelistPlayer] = []
137
+ for entry in whitelist_data:
138
+ player_uuid: str | None = entry.get("uuid", None)
139
+ player_name: str | None = entry.get("name", None)
140
+
141
+ if player_uuid is None or player_name is None:
142
+ error_message = f"Invalid whitelist entry!"
143
+ if player_uuid:
144
+ error_message += f" UUID: '{player_uuid}'"
145
+
146
+ if player_name:
147
+ error_message += f" Name: '{player_name}'"
148
+
149
+ log.error(error_message)
150
+ continue
151
+
152
+ try:
153
+ whitelist_player: WhitelistPlayer = {
154
+ "player_uuid": UUID(player_uuid),
155
+ "player_name": entry["name"],
156
+ }
157
+ except ValueError:
158
+ log.error(f"Invalid UUID in whitelist: '{player_uuid}'")
159
+ continue
160
+
161
+ whitelist_players.append(whitelist_player)
162
+
163
+ return whitelist_players