@@ -47,7 +47,7 @@ class Challenge(dict):
4747 "type" , "extra" , "image" , "protocol" , "host" ,
4848 "connection_info" , "healthcheck" , "attempts" , "flags" ,
4949 "files" , "topics" , "tags" , "files" , "hints" ,
50- "requirements" , "state" , "version" ,
50+ "requirements" , "next_id" , " state" , "version" ,
5151 # fmt: on
5252 ]
5353
@@ -103,6 +103,8 @@ def is_default_challenge_property(key: str, value: Any) -> bool:
103103 if key in ["tags" , "hints" , "topics" , "requirements" , "files" ] and value == []:
104104 return True
105105
106+ if key == "next_id" and value is None :
107+ return True
106108 return False
107109
108110 @staticmethod
@@ -434,6 +436,40 @@ def _set_required_challenges(self):
434436 r = self .api .patch (f"/api/v1/challenges/{ self .challenge_id } " , json = requirements_payload )
435437 r .raise_for_status ()
436438
439+ def _set_next_id (self , nid ):
440+ if type (nid ) == str :
441+ # nid by name
442+ # find the challenge id from installed challenges
443+ remote_challenges = self .load_installed_challenges ()
444+ for remote_challenge in remote_challenges :
445+ if remote_challenge ["name" ] == nid :
446+ nid = remote_challenge ["id" ]
447+ break
448+ if type (nid ) == str :
449+ click .secho (
450+ "Challenge cannot find next_id. Maybe it is invalid name or id. It will be cleared." ,
451+ fg = "yellow" ,
452+ )
453+ nid = None
454+ elif type (nid ) == int and nid > 0 :
455+ # nid by challenge id
456+ # trust it and use it directly
457+ nid = remote_challenge ["id" ]
458+ else :
459+ nid = None
460+
461+ if self .challenge_id == nid :
462+ click .secho (
463+ "Challenge cannot set next_id itself. Skipping invalid next_id." ,
464+ fg = "yellow" ,
465+ )
466+ nid = None
467+
468+ #return nid
469+ next_id_payload = {"next_id" : nid }
470+ r = self .api .patch (f"/api/v1/challenges/{ self .challenge_id } " , json = next_id_payload )
471+ r .raise_for_status ()
472+
437473 # Compare challenge requirements, will resolve all IDs to names
438474 def _compare_challenge_requirements (self , r1 : List [Union [str , int ]], r2 : List [Union [str , int ]]) -> bool :
439475 remote_challenges = self .load_installed_challenges ()
@@ -453,6 +489,21 @@ def normalize_requirements(requirements):
453489
454490 return normalize_requirements (r1 ) == normalize_requirements (r2 )
455491
492+ # Compare challenge next_id, will resolve all IDs to names
493+ def _compare_challenge_next_id (self , r1 : Union [str , int , None ], r2 : Union [str , int , None ]) -> bool :
494+ def normalize_next_id (r ):
495+ normalized = None
496+ if type (r ) == int :
497+ remote_challenge = self .load_installed_challenge (r )
498+ if remote_challenge ["id" ] == r :
499+ normalized = remote_challenge ["name" ]
500+ else :
501+ normalized = r
502+
503+ return normalized
504+
505+ return normalize_next_id (r1 ) == normalize_next_id (r2 )
506+
456507 # Normalize challenge data from the API response to match challenge.yml
457508 # It will remove any extra fields from the remote, as well as expand external references
458509 # that have to be fetched separately (e.g., files, flags, hints, etc.)
@@ -521,6 +572,16 @@ def _normalize_challenge(self, challenge_data: Dict[str, Any]):
521572 challenges = r .json ()["data" ]
522573 challenge ["requirements" ] = [c ["name" ] for c in challenges if c ["id" ] in requirements ]
523574
575+ # Add next_id
576+ nid = challenge_data .get ("next_id" , None )
577+ if nid :
578+ # Prefer challenge names over IDs
579+ r = self .api .get (f"/api/v1/challenges/{ nid } " )
580+ r .raise_for_status ()
581+ challenge ["next_id" ] = r .json ()["data" ]["name" ]
582+ else :
583+ challenge ["next_id" ] = None
584+
524585 return challenge
525586
526587 # Create a dictionary of remote files in { basename: {"url": "", "location": ""} } format
@@ -634,6 +695,11 @@ def sync(self, ignore: Tuple[str] = ()) -> None:
634695 if challenge .get ("requirements" ) and "requirements" not in ignore :
635696 self ._set_required_challenges ()
636697
698+ # Set next_id
699+ nid = challenge .get ("next_id" , None )
700+ if "next_id" not in ignore :
701+ self ._set_next_id (nid )
702+
637703 make_challenge_visible = False
638704
639705 # Bring back the challenge to be visible if:
@@ -711,6 +777,11 @@ def create(self, ignore: Tuple[str] = ()) -> None:
711777 if challenge .get ("requirements" ) and "requirements" not in ignore :
712778 self ._set_required_challenges ()
713779
780+ # Add next_id
781+ nid = challenge .get ("next_id" , None )
782+ if "next_id" not in ignore :
783+ self ._set_next_id (nid )
784+
714785 # Bring back the challenge if it's supposed to be visible
715786 # Either explicitly, or by assuming the default value (possibly because the state is ignored)
716787 if challenge .get ("state" , "visible" ) == "visible" or "state" in ignore :
@@ -864,6 +935,9 @@ def verify(self, ignore: Tuple[str] = ()) -> bool:
864935 if key == "requirements" :
865936 if self ._compare_challenge_requirements (challenge [key ], normalized_challenge [key ]):
866937 continue
938+ if key == "next_id" :
939+ if self ._compare_challenge_next_id (challenge [key ], normalized_challenge [key ]):
940+ continue
867941
868942 return False
869943
0 commit comments