Skip to content

Commit bed8f31

Browse files
authored
feat: Legacy file validation. (#16)
- Backwards compatible yuri's revenge upload - Make endpoints verify that `game` actually exists
1 parent 3947a78 commit bed8f31

File tree

15 files changed

+463
-71
lines changed

15 files changed

+463
-71
lines changed

kirovy/constants/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,18 @@ def is_messiah(cls, user_group: str) -> bool:
9090

9191

9292
class MigrationUser:
93-
ID = -1
93+
CNCNET_ID = -1
9494
USERNAME = "MobileConstructionVehicle_Migrator"
9595
GROUP = CncnetUserGroup.USER
9696

9797

98-
class GameSlugs(enum.StrEnum):
98+
class LegacyUploadUser:
99+
CNCNET_ID = -2
100+
USERNAME = "Spy_ShapeShifting_LegacyUploader"
101+
GROUP = CncnetUserGroup.USER
102+
103+
104+
class GameSlugs(str, enum.Enum):
99105
"""The slugs for each game / total conversion mod.
100106
101107
These **must** be unique. They are in constants because we need them to determine which parser to use

kirovy/constants/api_codes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@
33

44
class UploadApiCodes(enum.StrEnum):
55
GAME_SLUG_DOES_NOT_EXIST = "game-slug-does-not-exist"
6+
GAME_DOES_NOT_EXIST = "game-does-not-exist"
67
MISSING_GAME_SLUG = "missing-game-slug"
78
FILE_TO_LARGE = "file-too-large"
89
EMPTY_UPLOAD = "where-file"
910
DUPLICATE_MAP = "duplicate-map"
1011
FILE_EXTENSION_NOT_SUPPORTED = "file-extension-not-supported"
12+
13+
14+
class LegacyUploadApiCodes(enum.StrEnum):
15+
NOT_A_VALID_ZIP_FILE = "invalid-zipfile"
16+
BAD_ZIP_STRUCTURE = "invalid-zip-structure"
17+
MAP_TOO_LARGE = "map-file-too-large"
18+
NO_VALID_MAP_FILE = "no-valid-map-file"
19+
HASH_MISMATCH = "file-hash-does-not-match-zip-name"
20+
INVALID_FILE_TYPE = "invalid-file-type-in-zip"
21+
GAME_NOT_SUPPORTED = "game-not-supported"

kirovy/models/cnc_user.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ class CncUserManager(models.Manager):
1515
use_in_migrations = True
1616

1717
_SYSTEM_CNCNET_IDS = {
18-
constants.MigrationUser.ID,
18+
constants.MigrationUser.CNCNET_ID,
19+
constants.LegacyUploadUser.CNCNET_ID,
1920
}
2021

2122
def find_by_cncnet_id(self, cncnet_id: int) -> t.Tuple["CncUser"]:
@@ -27,10 +28,10 @@ def get_or_create_migration_user(self) -> "CncUser":
2728
:return:
2829
The user for running migrations.
2930
"""
30-
mcv = self.find_by_cncnet_id(constants.MigrationUser.ID)
31+
mcv = self.find_by_cncnet_id(constants.MigrationUser.CNCNET_ID)
3132
if not mcv:
3233
mcv = CncUser(
33-
cncnet_id=constants.MigrationUser.ID,
34+
cncnet_id=constants.MigrationUser.CNCNET_ID,
3435
username=constants.MigrationUser.USERNAME,
3536
group=constants.MigrationUser.GROUP,
3637
)
@@ -39,6 +40,30 @@ def get_or_create_migration_user(self) -> "CncUser":
3940

4041
return mcv
4142

43+
def get_or_create_legacy_upload_user(self) -> "CncUser":
44+
"""Gets or creates a system-user to represent anonymous uploads from a CnCNet client.
45+
46+
.. warning::
47+
48+
This should **only** be used for the legacy upload URLs for clients
49+
that CnCNet doesn't have the source for.
50+
51+
:return:
52+
User for legacy uploads.
53+
"""
54+
# If we copy and paste this again then it should be DRY'd up.
55+
spy = self.find_by_cncnet_id(constants.LegacyUploadUser.CNCNET_ID)
56+
if not spy:
57+
spy = CncUser(
58+
cncnet_id=constants.LegacyUploadUser.CNCNET_ID,
59+
username=constants.LegacyUploadUser.USERNAME,
60+
group=constants.LegacyUploadUser.GROUP,
61+
)
62+
spy.save()
63+
spy.refresh_from_db()
64+
65+
return spy
66+
4267
def get_queryset(self) -> models.QuerySet:
4368
"""Makes ``CncUser.object.all()`` filter out the system users by default.
4469
@@ -61,9 +86,7 @@ class CncUser(AbstractBaseUser):
6186
help_text=_("The user ID from the CNCNet ladder API."),
6287
)
6388

64-
username = models.CharField(
65-
null=True, help_text=_("The name from the CNCNet ladder API."), blank=False
66-
)
89+
username = models.CharField(null=True, help_text=_("The name from the CNCNet ladder API."), blank=False)
6790
""":attr: The username for debugging purposes. Don't rely on this field for much else."""
6891

6992
verified_map_uploader = models.BooleanField(null=False, default=False)
@@ -77,23 +100,15 @@ class CncUser(AbstractBaseUser):
77100
blank=False,
78101
)
79102

80-
is_banned = models.BooleanField(
81-
default=False, help_text="If true, user was banned for some reason."
82-
)
83-
ban_reason = models.CharField(
84-
default=None, null=True, help_text="If banned, the reason the user was banned."
85-
)
86-
ban_date = models.DateTimeField(
87-
default=None, null=True, help_text="If banned, when the user was banned."
88-
)
103+
is_banned = models.BooleanField(default=False, help_text="If true, user was banned for some reason.")
104+
ban_reason = models.CharField(default=None, null=True, help_text="If banned, the reason the user was banned.")
105+
ban_date = models.DateTimeField(default=None, null=True, help_text="If banned, when the user was banned.")
89106
ban_expires = models.DateTimeField(
90107
default=None,
91108
null=True,
92109
help_text="If banned, when the ban expires, if temporary.",
93110
)
94-
ban_count = models.IntegerField(
95-
default=0, help_text="How many times this user has been banned."
96-
)
111+
ban_count = models.IntegerField(default=0, help_text="How many times this user has been banned.")
97112

98113
USERNAME_FIELD = "cncnet_id"
99114
""":attr:
@@ -113,9 +128,7 @@ def can_upload(self) -> bool:
113128
:return:
114129
True if user can upload maps / mixes / big, or edit their existing uploads.
115130
"""
116-
self.refresh_from_db(
117-
fields=["verified_map_uploader", "verified_email", "is_banned"]
118-
)
131+
self.refresh_from_db(fields=["verified_map_uploader", "verified_email", "is_banned"])
119132
can_upload = self.verified_map_uploader or self.verified_email or self.is_staff
120133
return can_upload and not self.is_banned
121134

@@ -142,9 +155,7 @@ def create_or_update_from_cncnet(user_dto: CncnetUserInfo) -> "CncUser":
142155
:return:
143156
The user object in Kirovy's database, updated with the data from CnCNet.
144157
"""
145-
kirovy_user: t.Optional[CncUser] = CncUser.objects.filter(
146-
cncnet_id=user_dto.id
147-
).first()
158+
kirovy_user: t.Optional[CncUser] = CncUser.objects.filter(cncnet_id=user_dto.id).first()
148159
if not kirovy_user:
149160
kirovy_user = CncUser.objects.create(
150161
cncnet_id=user_dto.id,

kirovy/models/file_base.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ def validate_file_extension(self, file_extension: game_models.CncFileExtension)
8989
def save(self, *args, **kwargs):
9090
self.validate_file_extension(self.file_extension)
9191

92-
self.hash_md5 = file_utils.hash_file_md5(self.file)
93-
self.hash_sha512 = file_utils.hash_file_sha512(self.file)
94-
self.hash_sha1 = file_utils.hash_file_sha1(self.file)
92+
if not self.hash_md5:
93+
self.hash_md5 = file_utils.hash_file_md5(self.file)
94+
if not self.hash_sha512:
95+
self.hash_sha512 = file_utils.hash_file_sha512(self.file)
96+
if not self.hash_sha1:
97+
self.hash_sha1 = file_utils.hash_file_sha1(self.file)
9598
super().save(*args, **kwargs)
9699

97100
@staticmethod
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from kirovy.constants import GameSlugs
2+
from kirovy.constants.api_codes import LegacyUploadApiCodes
3+
from kirovy.exceptions import view_exceptions
4+
from kirovy.services.legacy_upload import westwood, dune_2000
5+
from kirovy.services.legacy_upload.base import LegacyMapServiceBase
6+
from kirovy import typing as t
7+
8+
9+
_GAME_LEGACY_SERVICE_MAP: t.Dict[str, t.Type[LegacyMapServiceBase]] = {
10+
GameSlugs.yuris_revenge.value: westwood.YurisRevengeLegacyMapService,
11+
GameSlugs.dune_2000.value: dune_2000.Dune2000LegacyMapService,
12+
}
13+
14+
15+
def get_legacy_service_for_slug(game_slug: str) -> t.Type[LegacyMapServiceBase]:
16+
if service := _GAME_LEGACY_SERVICE_MAP.get(game_slug):
17+
return service
18+
19+
raise view_exceptions.KirovyValidationError(
20+
"Game not supported on legacy endpoint",
21+
code=LegacyUploadApiCodes.GAME_NOT_SUPPORTED,
22+
additional={"supported": _GAME_LEGACY_SERVICE_MAP.keys()},
23+
)

0 commit comments

Comments
 (0)