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.
- mcserver/__init__.py +1 -0
- mcserver/__main__.py +3 -0
- mcserver/app.py +24 -0
- mcserver/caching.py +0 -0
- mcserver/cli.py +326 -0
- mcserver/commands/__init__.py +39 -0
- mcserver/commands/add_projects.py +7 -0
- mcserver/commands/import_setup.py +6 -0
- mcserver/commands/initialize_server.py +119 -0
- mcserver/commands/list_projects.py +6 -0
- mcserver/commands/op_grant.py +7 -0
- mcserver/commands/op_list.py +32 -0
- mcserver/commands/op_revoke.py +7 -0
- mcserver/commands/print_help.py +7 -0
- mcserver/commands/remove_projects.py +6 -0
- mcserver/commands/search_projects.py +7 -0
- mcserver/commands/server_status.py +6 -0
- mcserver/commands/show_projects.py +26 -0
- mcserver/commands/start_server.py +150 -0
- mcserver/commands/stop_server.py +46 -0
- mcserver/commands/update_projects.py +6 -0
- mcserver/commands/whitelist_add.py +7 -0
- mcserver/commands/whitelist_list.py +31 -0
- mcserver/commands/whitelist_remove.py +31 -0
- mcserver/commandsa.py +1297 -0
- mcserver/config.py +37 -0
- mcserver/constants.py +14 -0
- mcserver/fabricmc/meta.py +41 -0
- mcserver/legacyfabric/meta.py +37 -0
- mcserver/metadata.py +33 -0
- mcserver/minecraft/api.py +94 -0
- mcserver/minecraft/player_identity.py +21 -0
- mcserver/modrinth/api.py +657 -0
- mcserver/modrinth/modpack.py +101 -0
- mcserver/modrinth/project_records/metadata.py +0 -0
- mcserver/modrinth/project_records/relationships.py +203 -0
- mcserver/modrinth/project_records/shared.py +9 -0
- mcserver/modrinth/projects.py +80 -0
- mcserver/modrinth/shared.py +13 -0
- mcserver/modrinth/slug_index.py +36 -0
- mcserver/mojang/eula.py +28 -0
- mcserver/mojang/manifest.py +69 -0
- mcserver/mysterious.py +5 -0
- mcserver/networking.py +152 -0
- mcserver/papermc/api.py +81 -0
- mcserver/purpurmc/api.py +81 -0
- mcserver/quiltmc/meta.py +54 -0
- mcserver/resolver.py +164 -0
- mcserver/server_files/banned_ips.py +120 -0
- mcserver/server_files/banned_players.py +166 -0
- mcserver/server_files/ops.py +143 -0
- mcserver/server_files/properties.py +41 -0
- mcserver/server_files/usercaches.py +0 -0
- mcserver/server_files/whitelist.py +163 -0
- mcserver/shared.py +198 -0
- mcserver/state.py +77 -0
- mcserver_cli-3.0.0.dist-info/METADATA +63 -0
- mcserver_cli-3.0.0.dist-info/RECORD +61 -0
- mcserver_cli-3.0.0.dist-info/WHEEL +4 -0
- mcserver_cli-3.0.0.dist-info/entry_points.txt +2 -0
- mcserver_cli-3.0.0.dist-info/licenses/LICENSE +9 -0
mcserver/networking.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging as log
|
|
5
|
+
import shutil
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
|
|
11
|
+
# MCServer
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .shared import current_dir, format_number, mcserver_dir, print_status
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# TypedDict
|
|
17
|
+
class ResponseObject(TypedDict):
|
|
18
|
+
text: str
|
|
19
|
+
headers: dict
|
|
20
|
+
status: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Errors
|
|
24
|
+
class DownloadURLError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Variables
|
|
29
|
+
user_agent = f"Othecat35/MCServer/{__version__} (https://github.com/Othecat35/MCServer)"
|
|
30
|
+
read_chunk_size: int = 1024 * 64 # 64KiB
|
|
31
|
+
|
|
32
|
+
# Paths
|
|
33
|
+
tempfiles_dir = mcserver_dir / "tempfiles"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Functions
|
|
37
|
+
def request(
|
|
38
|
+
url: str,
|
|
39
|
+
query: dict | None = None,
|
|
40
|
+
data: bytes | None = None,
|
|
41
|
+
headers: dict | None = None,
|
|
42
|
+
method: str = "GET",
|
|
43
|
+
timeout: int = 10,
|
|
44
|
+
) -> ResponseObject:
|
|
45
|
+
method = method.upper()
|
|
46
|
+
if query is None:
|
|
47
|
+
query = {}
|
|
48
|
+
if headers is None:
|
|
49
|
+
headers = {}
|
|
50
|
+
headers.setdefault("User-Agent", user_agent)
|
|
51
|
+
|
|
52
|
+
query_string = f"?{urllib.parse.urlencode(query)}" if query else ""
|
|
53
|
+
request = urllib.request.Request(
|
|
54
|
+
f"{url}{query_string}", data=data, headers=headers, method=method
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
log.debug(f"Requesting URL: {method} {request.full_url}")
|
|
58
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
59
|
+
log.debug(f"Responded with status: {response.status} {response.reason}")
|
|
60
|
+
|
|
61
|
+
response_headers = {}
|
|
62
|
+
for key, value in response.getheaders():
|
|
63
|
+
response_headers[key.lower()] = value
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"text": response.read().decode("utf-8"),
|
|
67
|
+
"headers": response_headers,
|
|
68
|
+
"status": response.status,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def download(
|
|
73
|
+
url: str,
|
|
74
|
+
filename: str | Path,
|
|
75
|
+
hashes: dict | None = None,
|
|
76
|
+
headers: dict | None = None,
|
|
77
|
+
timeout: int = 10,
|
|
78
|
+
):
|
|
79
|
+
filename = Path(filename)
|
|
80
|
+
if hashes is None:
|
|
81
|
+
hashes = {}
|
|
82
|
+
if headers is None:
|
|
83
|
+
headers = {}
|
|
84
|
+
headers.setdefault("User-Agent", user_agent)
|
|
85
|
+
|
|
86
|
+
basename = filename.name
|
|
87
|
+
tempfile = tempfiles_dir / basename
|
|
88
|
+
|
|
89
|
+
request_url = urllib.request.Request(url, headers=headers, method="GET")
|
|
90
|
+
|
|
91
|
+
log.debug(f"Downloading URL: {request_url.full_url}")
|
|
92
|
+
log.debug(f"Temporary file: {tempfile}")
|
|
93
|
+
|
|
94
|
+
hash_algorithm = None
|
|
95
|
+
hash_name = None
|
|
96
|
+
|
|
97
|
+
if hashes.get("sha512"):
|
|
98
|
+
hash_algorithm = hashlib.sha512()
|
|
99
|
+
hash_name = "sha512"
|
|
100
|
+
elif hashes.get("sha1"):
|
|
101
|
+
hash_algorithm = hashlib.sha1()
|
|
102
|
+
hash_name = "sha1"
|
|
103
|
+
elif hashes.get("md5"):
|
|
104
|
+
hash_algorithm = hashlib.md5()
|
|
105
|
+
hash_name = "md5"
|
|
106
|
+
|
|
107
|
+
expected_hash = None
|
|
108
|
+
|
|
109
|
+
if hash_name:
|
|
110
|
+
expected_hash = hashes[hash_name]
|
|
111
|
+
log.debug(f"Calculating hash {hash_name} while downloading...")
|
|
112
|
+
log.debug(f"Expected {hash_name}: {expected_hash}")
|
|
113
|
+
|
|
114
|
+
with urllib.request.urlopen(request_url, timeout=timeout) as response:
|
|
115
|
+
content_length = int(response.getheader("Content-Length", default=0))
|
|
116
|
+
downloaded_length = 0
|
|
117
|
+
|
|
118
|
+
with open(tempfile, mode="wb") as file:
|
|
119
|
+
while True:
|
|
120
|
+
data = response.read(read_chunk_size)
|
|
121
|
+
|
|
122
|
+
if not data:
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
if hash_algorithm:
|
|
126
|
+
hash_algorithm.update(data)
|
|
127
|
+
|
|
128
|
+
downloaded_length += len(data)
|
|
129
|
+
|
|
130
|
+
if content_length == 0:
|
|
131
|
+
print_status(
|
|
132
|
+
f"Downloading {basename}... (unknown final size)",
|
|
133
|
+
dynamic=f"Downloading {basename}... {format_number(downloaded_length, 'iec')}",
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
print_status(
|
|
137
|
+
f"Downloading {basename}... File size: {format_number(content_length, 'iec')}",
|
|
138
|
+
dynamic=f"Downloading {basename}... {format_number(downloaded_length, 'iec')}/{format_number(content_length, 'iec')} ({round(downloaded_length / content_length * 100)}%)",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
file.write(data)
|
|
142
|
+
|
|
143
|
+
if hash_algorithm:
|
|
144
|
+
calculated_hash = hash_algorithm.hexdigest()
|
|
145
|
+
|
|
146
|
+
log.debug(f"Got hash: {calculated_hash}")
|
|
147
|
+
if calculated_hash != expected_hash:
|
|
148
|
+
raise DownloadURLError(
|
|
149
|
+
f"Downloaded file '{tempfile.relative_to(current_dir)}' hash does not match with the expected {hash_name} hash"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
shutil.move(tempfile, filename)
|
mcserver/papermc/api.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal, TypedDict
|
|
5
|
+
|
|
6
|
+
# MCServer
|
|
7
|
+
from .. import networking
|
|
8
|
+
from ..constants import papermc_api_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# TypedDicts
|
|
12
|
+
class FileHashes(TypedDict):
|
|
13
|
+
sha256: str # original: sha256
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BuildProp(TypedDict):
|
|
17
|
+
file_name: str # original: name
|
|
18
|
+
file_hashes: FileHashes # original: checksums
|
|
19
|
+
file_size: int # original: size
|
|
20
|
+
download_url: str # original: url
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BuildCommit(TypedDict):
|
|
24
|
+
commit_hash: str # original: hash
|
|
25
|
+
commit_time: str # original: time
|
|
26
|
+
commit_message: str # original: message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProjectBuild(TypedDict):
|
|
30
|
+
build_id: int # original: id
|
|
31
|
+
build_time: str # original: time
|
|
32
|
+
channel: str # original: channel
|
|
33
|
+
build_commits: list[BuildCommit] # original: commits
|
|
34
|
+
download_props: dict[str, BuildProp] # original: downloads
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Functions
|
|
38
|
+
def get_project_build(
|
|
39
|
+
project_name: str, game_version: str, build_id: int | Literal["latest"] = "latest"
|
|
40
|
+
) -> ProjectBuild:
|
|
41
|
+
response = networking.request(
|
|
42
|
+
f"{papermc_api_url}/v3/projects/{project_name}/versions/{game_version}/builds/{build_id}"
|
|
43
|
+
)
|
|
44
|
+
response_json = json.loads(response["text"])
|
|
45
|
+
|
|
46
|
+
build_commits: list[BuildCommit] = []
|
|
47
|
+
for commit in response_json["commits"]:
|
|
48
|
+
build_commit: BuildCommit = {
|
|
49
|
+
"commit_hash": commit["sha"],
|
|
50
|
+
"commit_time": commit["time"],
|
|
51
|
+
"commit_message": commit["message"],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
build_commits.append(build_commit)
|
|
55
|
+
|
|
56
|
+
download_props: dict[str, BuildProp] = {}
|
|
57
|
+
for prop_name, prop_data in response_json["downloads"].items():
|
|
58
|
+
download_prop: BuildProp = {
|
|
59
|
+
"file_name": prop_data["name"],
|
|
60
|
+
"file_hashes": prop_data["checksums"],
|
|
61
|
+
"file_size": prop_data["size"],
|
|
62
|
+
"download_url": prop_data["url"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
download_props[prop_name] = download_prop
|
|
66
|
+
|
|
67
|
+
# NOTE: probably use the "server:default" prop
|
|
68
|
+
|
|
69
|
+
project_build: ProjectBuild = {
|
|
70
|
+
"build_id": response_json["id"],
|
|
71
|
+
"build_time": response_json["time"],
|
|
72
|
+
"channel": response_json["channel"],
|
|
73
|
+
"build_commits": build_commits,
|
|
74
|
+
"download_props": download_props,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return project_build
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_latest_project_build(project_name: str, game_version: str) -> ProjectBuild:
|
|
81
|
+
return get_project_build(project_name, game_version, "latest")
|
mcserver/purpurmc/api.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal, TypedDict
|
|
5
|
+
|
|
6
|
+
# MCServer
|
|
7
|
+
from .. import networking
|
|
8
|
+
from ..constants import purpurmc_api_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# TypedDicts
|
|
12
|
+
class BuildCommit(TypedDict):
|
|
13
|
+
author_name: str # original: author
|
|
14
|
+
author_email: str # original: email
|
|
15
|
+
commit_message: str # original: description
|
|
16
|
+
commit_hash: str # original: hash
|
|
17
|
+
commit_time: int # original: timestamp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProjectBuild(TypedDict):
|
|
21
|
+
project_name: str # original: project
|
|
22
|
+
game_version: str # original: version
|
|
23
|
+
build_id: int # original: build
|
|
24
|
+
build_status: str # original: result
|
|
25
|
+
build_time: int # original: timestamp
|
|
26
|
+
build_duration: int # original: duration
|
|
27
|
+
build_commits: list[BuildCommit] # original: commits
|
|
28
|
+
metadata: dict # original: metadata
|
|
29
|
+
artifact_md5: str # original: md5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Functions
|
|
33
|
+
def get_project_build(
|
|
34
|
+
project_name: str, game_version: str, build_version: int | Literal["latest"]
|
|
35
|
+
) -> ProjectBuild:
|
|
36
|
+
response = networking.request(
|
|
37
|
+
f"{purpurmc_api_url}/v2/{project_name}/{game_version}/{build_version}"
|
|
38
|
+
)
|
|
39
|
+
response_data = json.loads(response["text"])
|
|
40
|
+
|
|
41
|
+
build_commits: list[BuildCommit] = []
|
|
42
|
+
for commit in response_data["commits"]:
|
|
43
|
+
build_commit: BuildCommit = {
|
|
44
|
+
"author_name": commit["author"],
|
|
45
|
+
"author_email": commit["email"],
|
|
46
|
+
"commit_message": commit["description"],
|
|
47
|
+
"commit_hash": commit["hash"],
|
|
48
|
+
"commit_time": commit["timestamp"],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
build_commits.append(build_commit)
|
|
52
|
+
|
|
53
|
+
project_build: ProjectBuild = {
|
|
54
|
+
"project_name": response_data["project"],
|
|
55
|
+
"game_version": response_data["version"],
|
|
56
|
+
"build_id": response_data["build"],
|
|
57
|
+
"build_status": response_data["result"],
|
|
58
|
+
"build_time": response_data["timestamp"],
|
|
59
|
+
"build_duration": response_data["duration"],
|
|
60
|
+
"build_commits": build_commits,
|
|
61
|
+
"metadata": response_data["metadata"],
|
|
62
|
+
"artifact_md5": response_data["md5"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return project_build
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_latest_project_build(project_name: str, game_version: str) -> ProjectBuild:
|
|
69
|
+
return get_project_build(project_name, game_version, "latest")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def download_url(
|
|
73
|
+
project_name: str, game_version: str, build_version: int | Literal["latest"]
|
|
74
|
+
) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"{purpurmc_api_url}/v2/{project_name}/{game_version}/{build_version}/download"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def latest_download_url(project_name: str, game_version: str) -> str:
|
|
81
|
+
return download_url(project_name, game_version, "latest")
|
mcserver/quiltmc/meta.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import json
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
# MCServer
|
|
7
|
+
from .. import networking
|
|
8
|
+
from ..constants import quiltmc_meta_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# TypedDicts
|
|
12
|
+
class Hashes(TypedDict):
|
|
13
|
+
sha1: str
|
|
14
|
+
sha256: str
|
|
15
|
+
sha512: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoaderVersion(TypedDict):
|
|
19
|
+
separator: str # this is the separator for the loader version
|
|
20
|
+
build_id: int
|
|
21
|
+
maven_id: str
|
|
22
|
+
loader_version: str
|
|
23
|
+
file_size: int
|
|
24
|
+
hashes: (
|
|
25
|
+
Hashes # what hash is this for? I don't know, please future me figure it out :>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Functions
|
|
30
|
+
def get_loader_versions() -> list[LoaderVersion]:
|
|
31
|
+
response = networking.request(f"{quiltmc_meta_url}/v3/versions/loader")
|
|
32
|
+
response_json = json.loads(response["text"])
|
|
33
|
+
|
|
34
|
+
loader_versions: list[LoaderVersion] = []
|
|
35
|
+
for version in response_json:
|
|
36
|
+
version_hash = version["hashes"]
|
|
37
|
+
hashes: Hashes = {
|
|
38
|
+
"sha1": version_hash["sha1"],
|
|
39
|
+
"sha256": version_hash["sha256"],
|
|
40
|
+
"sha512": version_hash["sha512"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
loader_version: LoaderVersion = {
|
|
44
|
+
"separator": version["separator"],
|
|
45
|
+
"build_id": version["build"],
|
|
46
|
+
"maven_id": version["maven"],
|
|
47
|
+
"loader_version": version["version"],
|
|
48
|
+
"file_size": version["file_size"],
|
|
49
|
+
"hashes": hashes,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
loader_versions.append(loader_version)
|
|
53
|
+
|
|
54
|
+
return loader_versions
|
mcserver/resolver.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import logging as log
|
|
2
|
+
from collections import deque
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Literal, TypedDict, TypeAlias
|
|
5
|
+
|
|
6
|
+
DependencyType: TypeAlias = Literal["embedded", "optional", "required", "incompatible"]
|
|
7
|
+
dependency_types: dict[DependencyType, int] = {
|
|
8
|
+
"embedded": 0,
|
|
9
|
+
"optional": 1,
|
|
10
|
+
"required": 2,
|
|
11
|
+
"incompatible": 3,
|
|
12
|
+
}
|
|
13
|
+
dependency_types2: list[DependencyType] = [
|
|
14
|
+
"embedded",
|
|
15
|
+
"optional",
|
|
16
|
+
"required",
|
|
17
|
+
"incompatible",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Dependency(TypedDict):
|
|
22
|
+
project_id: str
|
|
23
|
+
dependency_type: DependencyType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Project(TypedDict):
|
|
27
|
+
is_manual: bool
|
|
28
|
+
dependencies: dict[str, int]
|
|
29
|
+
dependents: dict[str, int]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProjectData(TypedDict):
|
|
33
|
+
project_id: str
|
|
34
|
+
is_manual: bool
|
|
35
|
+
dependencies: list[Dependency]
|
|
36
|
+
dependents: list[Dependency]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# NOTE: These are just theory, or not?
|
|
40
|
+
def human_to_resolver(dependencies: list[Dependency]) -> dict[str, int]:
|
|
41
|
+
resolver_dependencies: dict[str, int] = {}
|
|
42
|
+
for dependency in dependencies:
|
|
43
|
+
resolver_dependencies[dependency["project_id"]] = dependency_types[
|
|
44
|
+
dependency["dependency_type"]
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
return resolver_dependencies
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolver_to_human(dependency_graph: dict[str, Project]) -> list[ProjectData]:
|
|
51
|
+
projects: list[ProjectData] = []
|
|
52
|
+
for project_id, project_data in dependency_graph.items():
|
|
53
|
+
dependencies: list[Dependency] = []
|
|
54
|
+
for dependency_id, dependency_type in project_data["dependencies"].items():
|
|
55
|
+
dependency: Dependency = {
|
|
56
|
+
"project_id": dependency_id,
|
|
57
|
+
"dependency_type": dependency_types2[dependency_type],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dependencies.append(dependency)
|
|
61
|
+
|
|
62
|
+
dependents: list[Dependency] = []
|
|
63
|
+
for dependency_id, dependency_type in project_data["dependents"].items():
|
|
64
|
+
dependent: Dependency = {
|
|
65
|
+
"project_id": dependency_id,
|
|
66
|
+
"dependency_type": dependency_types2[dependency_type],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dependents.append(dependent)
|
|
70
|
+
|
|
71
|
+
project: ProjectData = {
|
|
72
|
+
"project_id": project_id,
|
|
73
|
+
"is_manual": project_data["is_manual"],
|
|
74
|
+
"dependencies": dependencies,
|
|
75
|
+
"dependents": dependents,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
projects.append(project)
|
|
79
|
+
|
|
80
|
+
return projects
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_dependencies(
|
|
84
|
+
project_ids: list[str] | str,
|
|
85
|
+
get_dependencies: Callable[[str], dict[str, int]],
|
|
86
|
+
should_queue: Callable[[int], bool],
|
|
87
|
+
) -> dict[str, Project]:
|
|
88
|
+
"""Resolve dependency tree using BFS
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
project_ids: Projects to start with
|
|
92
|
+
get_dependencies: Callback that returns a dictionary
|
|
93
|
+
should_queue: Callback that returns a bool
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
TypeError: Callback 'get_dependency' does not return a dictionary
|
|
97
|
+
"""
|
|
98
|
+
if isinstance(project_ids, str):
|
|
99
|
+
project_ids = [project_ids]
|
|
100
|
+
|
|
101
|
+
queued_projects: deque[str] = deque()
|
|
102
|
+
visited_projects: set[str] = set()
|
|
103
|
+
dependency_graph: dict[str, Project] = {}
|
|
104
|
+
|
|
105
|
+
for project_id in project_ids:
|
|
106
|
+
log.debug(f"Adding project '{project_id}' to the queue")
|
|
107
|
+
queued_projects.append(project_id)
|
|
108
|
+
project: Project = {"is_manual": True, "dependencies": {}, "dependents": {}}
|
|
109
|
+
dependency_graph[project_id] = project
|
|
110
|
+
|
|
111
|
+
while queued_projects:
|
|
112
|
+
project_id = queued_projects.popleft()
|
|
113
|
+
if project_id in visited_projects:
|
|
114
|
+
log.debug(f"Project '{project_id}' has already been visited")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
dependencies = get_dependencies(project_id)
|
|
118
|
+
dependency_graph[project_id]["dependencies"] = dependencies
|
|
119
|
+
|
|
120
|
+
for dependency_id, dependency_type in dependencies.items():
|
|
121
|
+
if dependency_id in visited_projects:
|
|
122
|
+
dependency_graph[dependency_id]["dependents"][
|
|
123
|
+
project_id
|
|
124
|
+
] = dependency_type
|
|
125
|
+
else:
|
|
126
|
+
dependency_graph[dependency_id] = {
|
|
127
|
+
"is_manual": False,
|
|
128
|
+
"dependencies": {},
|
|
129
|
+
"dependents": {project_id: dependency_type},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if should_queue(dependency_type):
|
|
133
|
+
log.debug(f"Adding dependency '{dependency_id}' to queue")
|
|
134
|
+
queued_projects.append(dependency_id)
|
|
135
|
+
|
|
136
|
+
visited_projects.add(project_id)
|
|
137
|
+
|
|
138
|
+
return dependency_graph
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def required_only(dependency_type: int) -> bool:
|
|
142
|
+
return dependency_type == dependency_types["required"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_dependencies(project_id: str) -> dict[str, int]:
|
|
146
|
+
dependencies = {
|
|
147
|
+
"embeddium": [{"project_id": "sodium", "dependency_type": "incompatible"}],
|
|
148
|
+
"fabric-api": [],
|
|
149
|
+
"origins": [{"project_id": "fabric-api", "dependency_type": "required"}],
|
|
150
|
+
"pehkui": [{"project_id": "fabric-api", "dependency_type": "required"}],
|
|
151
|
+
"podium": [{"project_id": "sodium", "dependency_type": "required"}],
|
|
152
|
+
"sodium": [],
|
|
153
|
+
"thdilos-fox-origin-expanded": [
|
|
154
|
+
{"project_id": "thdilos-fox-origin", "dependency_type": "required"},
|
|
155
|
+
{"project_id": "pehkui", "dependency_type": "required"},
|
|
156
|
+
{"project_id": "origins", "dependency_type": "required"},
|
|
157
|
+
],
|
|
158
|
+
"thdilos-fox-origin": [
|
|
159
|
+
{"project_id": "pehkui", "dependency_type": "required"},
|
|
160
|
+
{"project_id": "origins", "dependency_type": "required"},
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return human_to_resolver(dependencies[project_id])
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import json
|
|
4
|
+
import logging as log
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, TypedDict
|
|
7
|
+
|
|
8
|
+
# Paths
|
|
9
|
+
banned_ips_file = Path("banned-ips.json")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# TypedDicts
|
|
13
|
+
class BannedIPEntry(TypedDict):
|
|
14
|
+
ip: str | Literal["<unknown>"] # IPv4 or IPv6
|
|
15
|
+
created: str # Time format: yyyy-MM-dd HH:mm:ss Z
|
|
16
|
+
source: str
|
|
17
|
+
expires: Literal["forever"] | str # Time Format: yyyy-MM-dd HH:mm:ss Z
|
|
18
|
+
reason: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BannedIPRecord(TypedDict):
|
|
22
|
+
ip_address: str # IPv4 or IPv6
|
|
23
|
+
created_time: str # Time format: yyyy-MM-dd HH:mm:ss Z
|
|
24
|
+
ban_source: str
|
|
25
|
+
expires_time: str | Literal["forever"] # Time format: yyyy-MM-dd HH:mm:ss Z
|
|
26
|
+
ban_reason: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Functions
|
|
30
|
+
# Add IPs
|
|
31
|
+
def add_ip(
|
|
32
|
+
ip_address: str,
|
|
33
|
+
created_time: str,
|
|
34
|
+
ban_source: str,
|
|
35
|
+
expires_time: str | Literal["forever"] = "forever",
|
|
36
|
+
ban_reason: str = "Banned via MCServer.",
|
|
37
|
+
) -> None:
|
|
38
|
+
banned_ip_record: BannedIPRecord = {
|
|
39
|
+
"ip_address": ip_address,
|
|
40
|
+
"created_time": created_time,
|
|
41
|
+
"ban_source": ban_source,
|
|
42
|
+
"expires_time": expires_time,
|
|
43
|
+
"ban_reason": ban_reason,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
add_ips(banned_ip_record)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def add_ips(banned_ip_records: list[BannedIPRecord] | BannedIPRecord) -> None:
|
|
50
|
+
if not isinstance(banned_ip_records, list):
|
|
51
|
+
banned_ip_records = [banned_ip_records]
|
|
52
|
+
|
|
53
|
+
with open(banned_ips_file, mode="r+") as file:
|
|
54
|
+
banned_ips_data = json.load(file)
|
|
55
|
+
for ip_record in banned_ip_records:
|
|
56
|
+
banned_ip_entry: BannedIPEntry = {
|
|
57
|
+
"ip": ip_record["ip_address"],
|
|
58
|
+
"created": ip_record["created_time"],
|
|
59
|
+
"source": ip_record["ban_source"],
|
|
60
|
+
"expires": ip_record["expires_time"],
|
|
61
|
+
"reason": ip_record["ban_reason"],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
banned_ips_data.append(banned_ip_entry)
|
|
65
|
+
|
|
66
|
+
file.seek(0)
|
|
67
|
+
json.dump(banned_ips_data, file, indent=2)
|
|
68
|
+
file.truncate()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Remove IPs
|
|
72
|
+
def remove_ip(ip_address: str) -> None:
|
|
73
|
+
remove_ips(ip_address)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def remove_ips(ip_addresses: list[str] | str) -> None:
|
|
77
|
+
if not isinstance(ip_addresses, list):
|
|
78
|
+
ip_addresses = [ip_addresses]
|
|
79
|
+
|
|
80
|
+
unbanned_ip_addresses: set[str] = set()
|
|
81
|
+
for ip_address in ip_addresses:
|
|
82
|
+
unbanned_ip_addresses.add(ip_address)
|
|
83
|
+
|
|
84
|
+
with open(banned_ips_file, mode="r+") as file:
|
|
85
|
+
banned_ips_data: list[BannedIPEntry] = json.load(file)
|
|
86
|
+
new_banned_ips_data: list[BannedIPEntry] = []
|
|
87
|
+
for ip_entry in banned_ips_data:
|
|
88
|
+
ip_address = ip_entry["ip"]
|
|
89
|
+
if ip_address in unbanned_ip_addresses:
|
|
90
|
+
log.debug(
|
|
91
|
+
f"Removed IP address '{ip_address}' from the banned IP address list"
|
|
92
|
+
)
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
new_banned_ips_data.append(ip_entry)
|
|
96
|
+
|
|
97
|
+
file.seek(0)
|
|
98
|
+
json.dump(new_banned_ips_data, file, indent=2)
|
|
99
|
+
file.truncate()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# TODO: add 'update_ip' and 'update_ips', just use plain dict.update() because it has no depth
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# List IPs
|
|
106
|
+
def list_ips() -> list[BannedIPRecord]:
|
|
107
|
+
banned_ip_data: list[BannedIPEntry] = json.loads(banned_ips_file.read_text())
|
|
108
|
+
banned_ip_records: list[BannedIPRecord] = []
|
|
109
|
+
for ip_entry in banned_ip_data:
|
|
110
|
+
banned_ip_record: BannedIPRecord = {
|
|
111
|
+
"ip_address": ip_entry["ip"],
|
|
112
|
+
"created_time": ip_entry["created"],
|
|
113
|
+
"ban_source": ip_entry["source"],
|
|
114
|
+
"expires_time": ip_entry["expires"],
|
|
115
|
+
"ban_reason": ip_entry["reason"],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
banned_ip_records.append(banned_ip_record)
|
|
119
|
+
|
|
120
|
+
return banned_ip_records
|